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,77 +1,227 @@
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 DNSProviderForm from '../DNSProviderForm'
import { defaultProviderSchemas } from '../../data/dnsProviderSchemas'
import type { DNSProvider } from '../../api/dnsProviders'
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import DNSProviderForm from '../DNSProviderForm';
import userEvent from '@testing-library/user-event';
// Mock the hooks
const mockCreateMutation = {
mutateAsync: vi.fn(),
isPending: false,
};
const mockUpdateMutation = {
mutateAsync: vi.fn(),
isPending: false,
};
const mockTestCredentialsMutation = {
mutateAsync: vi.fn(),
isPending: false,
};
const mockEnableMultiCredentialsMutation = {
mutateAsync: vi.fn(),
isPending: false,
};
// Mock hooks used by DNSProviderForm
vi.mock('../../hooks/useDNSProviders', () => ({
useDNSProviderTypes: vi.fn(() => ({ data: [defaultProviderSchemas.script], isLoading: false })),
useDNSProviderMutations: vi.fn(() => ({ createMutation: { isPending: false }, updateMutation: { isPending: false }, testCredentialsMutation: { isPending: false } })),
}))
useDNSProviderTypes: vi.fn(() => ({
data: [
{
type: 'cloudflare',
name: 'Cloudflare',
fields: [
{ name: 'api_token', label: 'API Token', type: 'password', required: true }
]
},
{
type: 'route53',
name: 'Route53',
fields: [
{ name: 'access_key_id', label: 'Access Key ID', type: 'text', required: true },
{ name: 'secret_access_key', label: 'Secret Access Key', type: 'password', required: true }
]
}
],
isLoading: false,
})),
useDNSProviderMutations: vi.fn(() => ({
createMutation: mockCreateMutation,
updateMutation: mockUpdateMutation,
testCredentialsMutation: mockTestCredentialsMutation,
})),
}));
vi.mock('../../hooks/useCredentials', () => ({
useCredentials: vi.fn(() => ({ data: [] })),
useEnableMultiCredentials: vi.fn(() => ({ mutate: vi.fn(), isPending: false }))
}))
useEnableMultiCredentials: vi.fn(() => mockEnableMultiCredentialsMutation),
useCredentials: vi.fn(() => ({
data: [],
})),
}));
const renderWithClient = (ui: React.ReactElement) => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>)
}
// Mock CredentialManager component to avoid complex nested testing
vi.mock('../CredentialManager', () => ({
default: () => <div data-testid="credential-manager">Credential Manager Mock</div>,
}));
// Mock translations
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'dnsProviders.addProvider': 'Add DNS Provider',
'dnsProviders.editProvider': 'Edit DNS Provider',
'dnsProviders.providerName': 'Provider Name',
'dnsProviders.providerType': 'Provider Type',
'dnsProviders.propagationTimeout': 'Propagation Timeout (seconds)',
'dnsProviders.pollingInterval': 'Polling Interval (seconds)',
'dnsProviders.setAsDefault': 'Set as default provider',
'dnsProviders.advancedSettings': 'Advanced Settings',
'dnsProviders.testConnection': 'Test Connection',
'dnsProviders.testSuccess': 'Connection test successful',
'dnsProviders.testFailed': 'Connection test failed',
'common.create': 'Create',
'common.update': 'Update',
'common.cancel': 'Cancel',
};
return translations[key] || key;
},
}),
}));
describe('DNSProviderForm', () => {
const defaultProps = {
open: true,
onOpenChange: vi.fn(),
onSuccess: vi.fn(),
};
describe('DNSProviderForm — Script provider (accessibility)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
vi.clearAllMocks();
});
it('renders `Script Path` input when Script provider is selected (add flow)', async () => {
renderWithClient(<DNSProviderForm open={true} onOpenChange={() => {}} provider={null} onSuccess={() => {}} />)
it('renders correctly in add mode', () => {
render(<DNSProviderForm {...defaultProps} />);
// Open provider selector and choose the script provider
const select = screen.getByLabelText(/provider type/i)
await userEvent.click(select)
expect(screen.getByText('Add DNS Provider')).toBeInTheDocument();
expect(screen.getByLabelText('Provider Name')).toBeInTheDocument();
// Use role to find the trigger specifically
expect(screen.getByRole('combobox', { name: 'Provider Type' })).toBeInTheDocument();
});
const scriptOption = await screen.findByRole('option', { name: /script|custom script/i })
await userEvent.click(scriptOption)
// The input should be present, labelled "Script Path", have the expected placeholder and be required (add flow)
const scriptInput = await screen.findByRole('textbox', { name: /script path/i })
expect(scriptInput).toBeInTheDocument()
expect(scriptInput).toHaveAttribute('placeholder', expect.stringMatching(/dns-challenge\.sh/i))
expect(scriptInput).toBeRequired()
// Keyboard focus works
scriptInput.focus()
await waitFor(() => expect(scriptInput).toHaveFocus())
})
it('renders Script Path when editing an existing script provider (not required)', async () => {
const existingProvider: DNSProvider = {
it('populates fields when editing', async () => {
const provider = {
id: 1,
uuid: 'p-1',
name: 'local-script',
provider_type: 'script',
uuid: 'prov-uuid',
name: 'My Cloudflare',
provider_type: 'cloudflare' as const,
is_default: true,
enabled: true,
is_default: false,
propagation_timeout: 180,
polling_interval: 10,
has_credentials: true,
propagation_timeout: 120,
polling_interval: 5,
success_count: 0,
failure_count: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}
created_at: '2023-01-01',
updated_at: '2023-01-01',
};
renderWithClient(
<DNSProviderForm open={true} onOpenChange={() => {}} provider={existingProvider} onSuccess={() => {}} />
)
render(<DNSProviderForm {...defaultProps} provider={provider} />);
// Since provider prop is provided, providerType should be pre-populated and the field rendered
const scriptInput = await screen.findByRole('textbox', { name: /script path/i })
expect(scriptInput).toBeInTheDocument()
// Not required when editing
expect(scriptInput).not.toBeRequired()
})
})
expect(screen.getByText('Edit DNS Provider')).toBeInTheDocument();
expect(screen.getByDisplayValue('My Cloudflare')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByLabelText('API Token')).toBeInTheDocument();
});
});
it('handles form submission for creation', async () => {
const user = userEvent.setup();
render(<DNSProviderForm {...defaultProps} />);
await user.type(screen.getByLabelText('Provider Name'), 'New Provider');
const typeSelectTrigger = screen.getByRole('combobox', { name: 'Provider Type' });
await user.click(typeSelectTrigger);
// Select option by role to distinguish from trigger text
await user.click(screen.getByRole('option', { name: 'Cloudflare' }));
const tokenInput = await screen.findByLabelText('API Token');
await user.type(tokenInput, 'my-token');
mockCreateMutation.mutateAsync.mockResolvedValue({});
await user.click(screen.getByRole('button', { name: 'Create' }));
expect(mockCreateMutation.mutateAsync).toHaveBeenCalledWith(expect.objectContaining({
name: 'New Provider',
provider_type: 'cloudflare',
credentials: { api_token: 'my-token' },
}));
expect(defaultProps.onSuccess).toHaveBeenCalled();
});
it('handles validation failure (missing required fields)', async () => {
const user = userEvent.setup();
render(<DNSProviderForm {...defaultProps} />);
await user.type(screen.getByLabelText('Provider Name'), 'New Provider');
// Type is not selected, submit button should be disabled
const submitBtn = screen.getByRole('button', { name: 'Create' });
expect(submitBtn).toBeDisabled();
});
it('tests connection', async () => {
const user = userEvent.setup();
render(<DNSProviderForm {...defaultProps} />);
await user.type(screen.getByLabelText('Provider Name'), 'Test Prov');
await user.click(screen.getByRole('combobox', { name: 'Provider Type' }));
await user.click(screen.getByRole('option', { name: 'Cloudflare' }));
await user.type(screen.getByLabelText('API Token'), 'token');
mockTestCredentialsMutation.mutateAsync.mockResolvedValue({ success: true, message: 'Connection valid' });
await user.click(screen.getByRole('button', { name: 'Test Connection' }));
expect(mockTestCredentialsMutation.mutateAsync).toHaveBeenCalledWith(expect.objectContaining({
provider_type: 'cloudflare',
credentials: { api_token: 'token' }
}));
expect(await screen.findByText('Connection test successful')).toBeInTheDocument();
});
it('handles test connection failure', async () => {
const user = userEvent.setup();
render(<DNSProviderForm {...defaultProps} />);
await user.type(screen.getByLabelText('Provider Name'), 'Test Prov');
await user.click(screen.getByRole('combobox', { name: 'Provider Type' }));
await user.click(screen.getByRole('option', { name: 'Cloudflare' }));
await user.type(screen.getByLabelText('API Token'), 'token');
// Simulate error response structure
const errorResponse = {
response: { data: { error: 'Invalid token' } }
};
mockTestCredentialsMutation.mutateAsync.mockRejectedValue(errorResponse);
await user.click(screen.getByRole('button', { name: 'Test Connection' }));
expect(await screen.findByText('Connection test failed')).toBeInTheDocument();
expect(await screen.findByText('Invalid token')).toBeInTheDocument();
});
it('toggles advanced settings', async () => {
const user = userEvent.setup();
render(<DNSProviderForm {...defaultProps} />);
expect(screen.queryByLabelText('Propagation Timeout (seconds)')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Advanced Settings' }));
expect(screen.getByLabelText('Propagation Timeout (seconds)')).toBeInTheDocument();
expect(screen.getByLabelText('Polling Interval (seconds)')).toBeInTheDocument();
expect(screen.getByLabelText('Set as default provider')).toBeInTheDocument();
});
});