fix(tests): Enhance CrowdSecConfig with new input fields and improve accessibility

- Added IDs to input fields in CrowdSecConfig for better accessibility.
- Updated labels to use <label> elements for checkboxes and inputs.
- Improved error handling and user feedback in the CrowdSecConfig tests.
- Enhanced test coverage for console enrollment and banned IP functionalities.

fix: Update SecurityHeaders to include aria-label for delete button

- Added aria-label to the delete button for better screen reader support.

test: Add comprehensive tests for proxyHostsHelpers and validation utilities

- Implemented tests for formatting and help text functions in proxyHostsHelpers.
- Added validation tests for email and IP address formats.

chore: Update vitest configuration for dynamic coverage thresholds

- Adjusted coverage thresholds to be dynamic based on environment variables.
- Included additional coverage reporters.

chore: Update frontend-test-coverage script to reflect new coverage threshold

- Increased minimum coverage requirement from 85% to 87.5%.

fix: Ensure tests pass with consistent data in passwd file

- Updated tests/etc/passwd to ensure consistent content.
This commit is contained in:
GitHub Actions
2026-02-06 17:38:08 +00:00
parent 57c3a70007
commit 10582872f9
34 changed files with 4197 additions and 724 deletions

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
@@ -60,6 +60,50 @@ vi.mock('../../hooks/useCertificates', () => ({
})),
}))
vi.mock('../../hooks/useAccessLists', () => ({
useAccessLists: vi.fn(() => ({
data: [
{ id: 10, name: 'Trusted IPs', type: 'allow_list', enabled: true, description: 'Only trusted' },
{ id: 11, name: 'Geo Block', type: 'geo_block', enabled: true }
],
isLoading: false,
error: null,
})),
}))
vi.mock('../../hooks/useSecurityHeaders', () => ({
useSecurityHeaderProfiles: vi.fn(() => ({
data: [
{ id: 100, name: 'Strict Profile', description: 'Very strict', security_score: 90, is_preset: true, preset_type: 'strict' }
],
isLoading: false,
error: null,
})),
}))
vi.mock('../../hooks/useDNSDetection', () => ({
useDetectDNSProvider: vi.fn(() => ({
mutateAsync: vi.fn(),
isPending: false,
data: null,
reset: vi.fn(),
})),
}))
vi.mock('../../hooks/useDNSProviders', () => ({
useDNSProviders: vi.fn(() => ({
data: [
{ id: 1, name: 'Cloudflare', provider_type: 'cloudflare', enabled: true, has_credentials: true }
],
isLoading: false,
error: null,
})),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock('../../hooks/useSecurity', () => ({
useAuthPolicies: vi.fn(() => ({
policies: [
@@ -657,4 +701,530 @@ describe('ProxyHostForm', () => {
})
})
})
describe('Security Options', () => {
it('toggles security options', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Toggle SSL Forced (default is true)
const sslCheckbox = screen.getByLabelText('Force SSL')
expect(sslCheckbox).toBeChecked()
await userEvent.click(sslCheckbox)
expect(sslCheckbox).not.toBeChecked()
// Toggle HSTS (default is true)
const hstsCheckbox = screen.getByLabelText('HSTS Enabled')
expect(hstsCheckbox).toBeChecked()
await userEvent.click(hstsCheckbox)
expect(hstsCheckbox).not.toBeChecked()
// Toggle HTTP/2 (default is true)
const http2Checkbox = screen.getByLabelText('HTTP/2 Support')
expect(http2Checkbox).toBeChecked()
await userEvent.click(http2Checkbox)
expect(http2Checkbox).not.toBeChecked()
// Toggle Block Exploits (default is true)
const blockExploitsCheckbox = screen.getByLabelText('Block Exploits')
expect(blockExploitsCheckbox).toBeChecked()
await userEvent.click(blockExploitsCheckbox)
expect(blockExploitsCheckbox).not.toBeChecked()
})
})
describe('Access Control', () => {
it('selects an access list', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Select 'Trusted IPs'
// Need to find the select by label/aria-label? AccessListSelector has label "Access Control List"
const aclSelect = screen.getByLabelText(/Access Control List/i)
await userEvent.selectOptions(aclSelect, '10')
// Verify it was selected
expect(aclSelect).toHaveValue('10')
// Verify description appears
expect(screen.getByText('Trusted IPs')).toBeInTheDocument()
expect(screen.getByText('Only trusted')).toBeInTheDocument()
})
})
describe('Wildcard Domains', () => {
it('shows DNS provider selector for wildcard domains', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Enter a wildcard domain
const domainInput = screen.getByPlaceholderText('example.com, www.example.com')
await userEvent.type(domainInput, '*.example.com')
// DNS Provider Selector should appear
await waitFor(() => {
expect(screen.getByTestId('dns-provider-section')).toBeInTheDocument()
})
// Select a provider using the mocked data: Cloudflare (ID 1)
const section = screen.getByTestId('dns-provider-section')
// Since Shadcn Select uses Radix, the trigger is a button with role combobox
const providerSelectTrigger = within(section).getByRole('combobox')
await userEvent.click(providerSelectTrigger)
const cloudflareOption = screen.getByText('Cloudflare')
await userEvent.click(cloudflareOption)
// Now try to save
await userEvent.type(screen.getByLabelText(/^Name/), 'Wildcard Test')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.10')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
domain_names: '*.example.com',
dns_provider_id: 1
}))
})
})
it('validates DNS provider requirement for wildcard domains', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Enter a wildcard domain
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), '*.missing.com')
// Fill other required fields
await userEvent.type(screen.getByLabelText(/^Name/), 'Missing Provider')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.10')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
// Click save without selecting provider
await userEvent.click(screen.getByText('Save'))
// Expect toast error (mocked only effectively if we check for it, but here we check it prevents submit)
expect(mockOnSubmit).not.toHaveBeenCalled()
})
})
// ===== BRANCH COVERAGE EXPANSION TESTS =====
describe('Form Submission and Validation', () => {
it('prevents submission without required fields', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Click save without filling any fields
await userEvent.click(screen.getByText('Save'))
// Submit should not be called
expect(mockOnSubmit).not.toHaveBeenCalled()
})
it('submits form with all basic fields', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'My Service')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'myservice.com')
await userEvent.selectOptions(screen.getByLabelText(/^Scheme/), 'https')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.clear(screen.getByLabelText(/^Port/))
await userEvent.type(screen.getByLabelText(/^Port/), '8080')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
name: 'My Service',
domain_names: 'myservice.com',
forward_scheme: 'https',
forward_host: '192.168.1.100',
forward_port: 8080,
}))
})
})
it('submits form with certificate selection', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Cert Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'cert.example.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
// Select certificate
const certSelect = screen.getByLabelText(/Certificate/i)
await userEvent.selectOptions(certSelect, '1')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
certificate_id: 1,
}))
})
})
it('submits form with security header profile selection', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Security Headers Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'secure.example.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
// Select security header profile
const profileSelect = screen.getByLabelText(/Security Headers/i)
await userEvent.selectOptions(profileSelect, '100')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
security_header_profile_id: 100,
}))
})
})
})
describe('Edit Mode vs Create Mode', () => {
it('shows edit mode with pre-filled data', async () => {
const existingHost: ProxyHost = {
uuid: 'host-uuid-1',
name: 'Existing Service',
domain_names: 'existing.com',
forward_scheme: 'https' as const,
forward_host: '192.168.1.50',
forward_port: 443,
ssl_forced: true,
http2_support: true,
hsts_enabled: true,
hsts_subdomains: true,
block_exploits: true,
websocket_support: false,
enable_standard_headers: true,
application: 'none' as const,
advanced_config: '',
enabled: true,
locations: [],
certificate_id: null,
access_list_id: null,
security_header_profile_id: null,
dns_provider_id: null,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
await renderWithClientAct(
<ProxyHostForm host={existingHost} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fields should be pre-filled
expect(screen.getByDisplayValue('Existing Service')).toBeInTheDocument()
expect(screen.getByDisplayValue('existing.com')).toBeInTheDocument()
expect(screen.getByDisplayValue('192.168.1.50')).toBeInTheDocument()
// Update and save
const nameInput = screen.getByDisplayValue('Existing Service') as HTMLInputElement
await userEvent.clear(nameInput)
await userEvent.type(nameInput, 'Updated Service')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
name: 'Updated Service',
}))
})
})
it('renders title as Edit for existing host', async () => {
const existingHost: ProxyHost = {
uuid: 'host-uuid-1',
name: 'Existing',
domain_names: 'test.com',
forward_scheme: 'http' as const,
forward_host: '127.0.0.1',
forward_port: 80,
ssl_forced: true,
http2_support: true,
hsts_enabled: true,
hsts_subdomains: true,
block_exploits: true,
websocket_support: false,
enable_standard_headers: true,
application: 'none' as const,
advanced_config: '',
enabled: true,
locations: [],
certificate_id: null,
access_list_id: null,
security_header_profile_id: null,
dns_provider_id: null,
created_at: '',
updated_at: '',
}
await renderWithClientAct(
<ProxyHostForm host={existingHost} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
expect(screen.getByText('Edit Proxy Host')).toBeInTheDocument()
})
it('renders title as Add for new proxy host', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
expect(screen.getByText('Add Proxy Host')).toBeInTheDocument()
})
})
describe('Scheme Selection', () => {
it('shows scheme options http, https, ws, wss', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const schemeSelect = screen.getByLabelText('Scheme')
expect(schemeSelect).toBeInTheDocument()
const options = schemeSelect.querySelectorAll('option')
const values = Array.from(options).map(o => o.value)
expect(values).toContain('http')
expect(values).toContain('https')
expect(values).toContain('ws')
expect(values).toContain('wss')
})
it('accepts websockets scheme', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'WS Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'ws.example.com')
await userEvent.selectOptions(screen.getByLabelText('Scheme') as HTMLSelectElement, 'ws')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '8000')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
forward_scheme: 'ws',
}))
})
})
it('accepts secure websockets scheme', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'WSS Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'wss.example.com')
await userEvent.selectOptions(screen.getByLabelText('Scheme') as HTMLSelectElement, 'wss')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '8000')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
forward_scheme: 'wss',
}))
})
})
})
describe('Cancel Operations', () => {
it('calls onCancel when cancel button is clicked', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const cancelBtn = screen.getByRole('button', { name: /Cancel/i })
await userEvent.click(cancelBtn)
expect(mockOnCancel).toHaveBeenCalled()
})
})
describe('Advanced Config', () => {
it('shows advanced config field for application presets', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Select Plex preset
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
// Find advanced config field (it's in a collapsible section)
// Check that advanced config JSON for plex has been populated
const advancedConfigField = screen.getByPlaceholderText(/Caddy JSON config/i) as HTMLTextAreaElement
// Verify it contains JSON (Plex has some default config)
if (advancedConfigField.value) {
expect(advancedConfigField.value).toContain('handler')
}
})
it('allows manual advanced config input', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Custom Config')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'custom.example.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
const advancedConfigField = screen.getByPlaceholderText('Additional Caddy directives...')
await userEvent.type(advancedConfigField, 'header /api/* X-Custom-Header "test"')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
advanced_config: expect.stringContaining('header'),
}))
})
})
})
describe('Port Input Handling', () => {
it('validates port number range', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Port Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'port.example.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
// Clear and set invalid port
const portInput = screen.getByLabelText(/^Port$/) as HTMLInputElement
await userEvent.clear(portInput)
await userEvent.type(portInput, '99999')
// The form should still allow submission (validation happens server-side usually)
// But port should be converted to number
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalled()
})
})
})
describe('Host and Port Combination', () => {
it('accepts docker container IP', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Docker Container')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'docker.example.com')
await userEvent.type(screen.getByLabelText(/^Host/), '172.17.0.2')
await userEvent.type(screen.getByLabelText(/^Port/), '8080')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
forward_host: '172.17.0.2',
}))
})
})
it('accepts localhost IP', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Localhost')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'localhost.example.com')
await userEvent.type(screen.getByLabelText(/^Host/), 'localhost')
await userEvent.type(screen.getByLabelText(/^Port/), '3000')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
forward_host: 'localhost',
}))
})
})
})
describe('Enabled/Disabled State', () => {
it('toggles enabled state', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Enabled Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'enabled.example.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
// Toggle enabled (defaults to true) - look for "Enable Proxy Host" text
const enabledCheckbox = screen.getByLabelText(/Enable Proxy Host/)
await userEvent.click(enabledCheckbox)
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
enabled: false,
}))
})
})
})
describe('Standard Headers Option', () => {
it('toggles standard headers option', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const standardHeadersCheckbox = screen.getByLabelText(/Enable Standard Proxy Headers/)
expect(standardHeadersCheckbox).toBeChecked()
await userEvent.click(standardHeadersCheckbox)
expect(standardHeadersCheckbox).not.toBeChecked()
await userEvent.type(screen.getByLabelText(/^Name/), 'No Headers')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'no-headers.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
enable_standard_headers: false,
}))
})
})
})
})