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:
139
frontend/src/api/__tests__/client.test.ts
Normal file
139
frontend/src/api/__tests__/client.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'
|
||||
|
||||
type ResponseHandler = (value: unknown) => unknown
|
||||
type ErrorHandler = (error: ResponseError) => Promise<never>
|
||||
|
||||
type ResponseError = {
|
||||
response?: {
|
||||
status?: number
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
config?: {
|
||||
url?: string
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
|
||||
// Use vi.hoisted() to declare variables accessible in hoisted mocks
|
||||
const capturedHandlers = vi.hoisted(() => ({
|
||||
onFulfilled: undefined as ResponseHandler | undefined,
|
||||
onRejected: undefined as ErrorHandler | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('axios', () => {
|
||||
const mockClient = {
|
||||
defaults: {
|
||||
headers: {
|
||||
common: {} as Record<string, string>,
|
||||
},
|
||||
},
|
||||
interceptors: {
|
||||
response: {
|
||||
use: vi.fn((onFulfilled?: ResponseHandler, onRejected?: ErrorHandler) => {
|
||||
capturedHandlers.onFulfilled = onFulfilled
|
||||
capturedHandlers.onRejected = onRejected
|
||||
return vi.fn()
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
default: {
|
||||
create: vi.fn(() => mockClient),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Must import AFTER mock definition
|
||||
import { setAuthErrorHandler, setAuthToken } from '../client'
|
||||
import axios from 'axios'
|
||||
|
||||
// Get mock client instance for header assertions
|
||||
const getMockClient = () => {
|
||||
const mockAxios = vi.mocked(axios)
|
||||
return mockAxios.create()
|
||||
}
|
||||
|
||||
describe('api client', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('sets and clears the Authorization header', () => {
|
||||
const mockClientInstance = getMockClient()
|
||||
|
||||
setAuthToken('test-token')
|
||||
expect(mockClientInstance.defaults.headers.common.Authorization).toBe('Bearer test-token')
|
||||
|
||||
setAuthToken(null)
|
||||
expect(mockClientInstance.defaults.headers.common.Authorization).toBeUndefined()
|
||||
})
|
||||
|
||||
it('extracts error message from response payload', async () => {
|
||||
const error: ResponseError = {
|
||||
response: { data: { error: 'Bad request' } },
|
||||
config: { url: '/test' },
|
||||
message: 'Original',
|
||||
}
|
||||
|
||||
const handler = capturedHandlers.onRejected
|
||||
expect(handler).toBeDefined()
|
||||
|
||||
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
||||
|
||||
await expect(resultPromise).rejects.toBe(error)
|
||||
expect(error.message).toBe('Bad request')
|
||||
})
|
||||
|
||||
it('invokes auth error handler on 401 outside auth endpoints', async () => {
|
||||
const onAuthError = vi.fn()
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
setAuthErrorHandler(onAuthError)
|
||||
|
||||
const error: ResponseError = {
|
||||
response: { status: 401, data: { message: 'Unauthorized' } },
|
||||
config: { url: '/proxy-hosts' },
|
||||
message: 'Original',
|
||||
}
|
||||
|
||||
const handler = capturedHandlers.onRejected
|
||||
expect(handler).toBeDefined()
|
||||
|
||||
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
||||
|
||||
await expect(resultPromise).rejects.toBe(error)
|
||||
expect(onAuthError).toHaveBeenCalledTimes(1)
|
||||
expect(error.message).toBe('Unauthorized')
|
||||
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('skips auth error handler for auth endpoints', async () => {
|
||||
const onAuthError = vi.fn()
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
setAuthErrorHandler(onAuthError)
|
||||
|
||||
const error: ResponseError = {
|
||||
response: { status: 401, data: { message: 'Unauthorized' } },
|
||||
config: { url: '/auth/login' },
|
||||
message: 'Original',
|
||||
}
|
||||
|
||||
const handler = capturedHandlers.onRejected
|
||||
expect(handler).toBeDefined()
|
||||
|
||||
// Call handler with auth endpoint error to verify it skips the auth error handler
|
||||
if (handler) {
|
||||
await handler(error)
|
||||
}
|
||||
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
85
frontend/src/api/__tests__/import.test.ts
Normal file
85
frontend/src/api/__tests__/import.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { uploadCaddyfile, uploadCaddyfilesMulti, getImportPreview, commitImport, cancelImport, getImportStatus } from '../import';
|
||||
import client from '../client';
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('import API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('uploadCaddyfile posts content', async () => {
|
||||
const content = 'example.com';
|
||||
const mockResponse = { preview: { hosts: [] } };
|
||||
(client.post as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await uploadCaddyfile(content);
|
||||
expect(client.post).toHaveBeenCalledWith('/import/upload', { content });
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('uploadCaddyfilesMulti posts files', async () => {
|
||||
const files = [{ filename: 'Caddyfile', content: 'foo.com' }];
|
||||
const mockResponse = { preview: { hosts: [] } };
|
||||
(client.post as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await uploadCaddyfilesMulti(files);
|
||||
expect(client.post).toHaveBeenCalledWith('/import/upload-multi', { files });
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('getImportPreview gets preview', async () => {
|
||||
const mockResponse = { preview: { hosts: [] } };
|
||||
(client.get as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await getImportPreview();
|
||||
expect(client.get).toHaveBeenCalledWith('/import/preview');
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('commitImport posts commitments', async () => {
|
||||
const sessionUUID = 'uuid-123';
|
||||
const resolutions = { 'foo.com': 'keep' };
|
||||
const names = { 'foo.com': 'My Site' };
|
||||
const mockResponse = { created: 1, updated: 0, skipped: 0, errors: [] };
|
||||
|
||||
(client.post as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await commitImport(sessionUUID, resolutions, names);
|
||||
expect(client.post).toHaveBeenCalledWith('/import/commit', {
|
||||
session_uuid: sessionUUID,
|
||||
resolutions,
|
||||
names
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('cancelImport posts cancel', async () => {
|
||||
(client.post as any).mockResolvedValue({});
|
||||
|
||||
await cancelImport();
|
||||
expect(client.post).toHaveBeenCalledWith('/import/cancel');
|
||||
});
|
||||
|
||||
it('getImportStatus gets status', async () => {
|
||||
const mockResponse = { has_pending: true };
|
||||
(client.get as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await getImportStatus();
|
||||
expect(client.get).toHaveBeenCalledWith('/import/status');
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('getImportStatus handles error', async () => {
|
||||
(client.get as any).mockRejectedValue(new Error('Failed'));
|
||||
|
||||
const result = await getImportStatus();
|
||||
expect(result).toEqual({ has_pending: false });
|
||||
});
|
||||
});
|
||||
122
frontend/src/api/__tests__/plugins.test.ts
Normal file
122
frontend/src/api/__tests__/plugins.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import client from '../client';
|
||||
import {
|
||||
getPlugins,
|
||||
getPlugin,
|
||||
enablePlugin,
|
||||
disablePlugin,
|
||||
reloadPlugins,
|
||||
type PluginInfo,
|
||||
} from '../plugins';
|
||||
|
||||
// Mock the API client
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Plugins API', () => {
|
||||
const mockPlugins: PluginInfo[] = [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'plugin-1',
|
||||
name: 'Test Plugin 1',
|
||||
type: 'auth',
|
||||
enabled: true,
|
||||
status: 'loaded',
|
||||
is_built_in: false,
|
||||
created_at: '2023-01-01',
|
||||
updated_at: '2023-01-01',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
uuid: 'plugin-2',
|
||||
name: 'Test Plugin 2',
|
||||
type: 'notification',
|
||||
enabled: false,
|
||||
status: 'pending',
|
||||
is_built_in: true,
|
||||
created_at: '2023-01-01',
|
||||
updated_at: '2023-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getPlugins', () => {
|
||||
it('fetches all plugins successfully', async () => {
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPlugins });
|
||||
|
||||
const result = await getPlugins();
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/plugins');
|
||||
expect(result).toEqual(mockPlugins);
|
||||
});
|
||||
|
||||
it('propagates error when request fails', async () => {
|
||||
const error = new Error('API Error');
|
||||
vi.mocked(client.get).mockRejectedValueOnce(error);
|
||||
|
||||
await expect(getPlugins()).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlugin', () => {
|
||||
it('fetches a single plugin successfully', async () => {
|
||||
const plugin = mockPlugins[0];
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: plugin });
|
||||
|
||||
const result = await getPlugin(1);
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/plugins/1');
|
||||
expect(result).toEqual(plugin);
|
||||
});
|
||||
|
||||
it('propagates error when plugin not found', async () => {
|
||||
const error = new Error('Not Found');
|
||||
vi.mocked(client.get).mockRejectedValueOnce(error);
|
||||
|
||||
await expect(getPlugin(999)).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enablePlugin', () => {
|
||||
it('enables a plugin successfully', async () => {
|
||||
const response = { message: 'Plugin enabled' };
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: response });
|
||||
|
||||
const result = await enablePlugin(1);
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/plugins/1/enable');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disablePlugin', () => {
|
||||
it('disables a plugin successfully', async () => {
|
||||
const response = { message: 'Plugin disabled' };
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: response });
|
||||
|
||||
const result = await disablePlugin(1);
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/plugins/1/disable');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloadPlugins', () => {
|
||||
it('reloads plugins successfully', async () => {
|
||||
const response = { message: 'Plugins reloaded', count: 5 };
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: response });
|
||||
|
||||
const result = await reloadPlugins();
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/plugins/reload');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
112
frontend/src/api/__tests__/securityHeaders.test.ts
Normal file
112
frontend/src/api/__tests__/securityHeaders.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { securityHeadersApi } from '../securityHeaders';
|
||||
import client from '../client';
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('securityHeadersApi', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('listProfiles returns profiles', async () => {
|
||||
const mockProfiles = [{ id: 1, name: 'Profile 1' }];
|
||||
(client.get as any).mockResolvedValue({ data: { profiles: mockProfiles } });
|
||||
|
||||
const result = await securityHeadersApi.listProfiles();
|
||||
expect(client.get).toHaveBeenCalledWith('/security/headers/profiles');
|
||||
expect(result).toEqual(mockProfiles);
|
||||
});
|
||||
|
||||
it('getProfile returns a profile', async () => {
|
||||
const mockProfile = { id: 1, name: 'Profile 1' };
|
||||
(client.get as any).mockResolvedValue({ data: { profile: mockProfile } });
|
||||
|
||||
const result = await securityHeadersApi.getProfile(1);
|
||||
expect(client.get).toHaveBeenCalledWith('/security/headers/profiles/1');
|
||||
expect(result).toEqual(mockProfile);
|
||||
});
|
||||
|
||||
it('createProfile creates a profile', async () => {
|
||||
const newProfile = { name: 'New Profile' };
|
||||
const mockResponse = { id: 1, ...newProfile };
|
||||
(client.post as any).mockResolvedValue({ data: { profile: mockResponse } });
|
||||
|
||||
const result = await securityHeadersApi.createProfile(newProfile);
|
||||
expect(client.post).toHaveBeenCalledWith('/security/headers/profiles', newProfile);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('updateProfile updates a profile', async () => {
|
||||
const updates = { name: 'Updated Profile' };
|
||||
const mockResponse = { id: 1, ...updates };
|
||||
(client.put as any).mockResolvedValue({ data: { profile: mockResponse } });
|
||||
|
||||
const result = await securityHeadersApi.updateProfile(1, updates);
|
||||
expect(client.put).toHaveBeenCalledWith('/security/headers/profiles/1', updates);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('deleteProfile deletes a profile', async () => {
|
||||
(client.delete as any).mockResolvedValue({});
|
||||
|
||||
await securityHeadersApi.deleteProfile(1);
|
||||
expect(client.delete).toHaveBeenCalledWith('/security/headers/profiles/1');
|
||||
});
|
||||
|
||||
it('getPresets returns presets', async () => {
|
||||
const mockPresets = [{ name: 'Basic' }];
|
||||
(client.get as any).mockResolvedValue({ data: { presets: mockPresets } });
|
||||
|
||||
const result = await securityHeadersApi.getPresets();
|
||||
expect(client.get).toHaveBeenCalledWith('/security/headers/presets');
|
||||
expect(result).toEqual(mockPresets);
|
||||
});
|
||||
|
||||
it('applyPreset applies a preset', async () => {
|
||||
const request = { preset_type: 'basic', name: 'My Preset' };
|
||||
const mockResponse = { id: 1, ...request };
|
||||
(client.post as any).mockResolvedValue({ data: { profile: mockResponse } });
|
||||
|
||||
const result = await securityHeadersApi.applyPreset(request);
|
||||
expect(client.post).toHaveBeenCalledWith('/security/headers/presets/apply', request);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('calculateScore calculates score', async () => {
|
||||
const config = { hsts_enabled: true };
|
||||
const mockResponse = { score: 90 };
|
||||
(client.post as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await securityHeadersApi.calculateScore(config);
|
||||
expect(client.post).toHaveBeenCalledWith('/security/headers/score', config);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('validateCSP validates CSP', async () => {
|
||||
const csp = "default-src 'self'";
|
||||
const mockResponse = { valid: true, errors: [] };
|
||||
(client.post as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await securityHeadersApi.validateCSP(csp);
|
||||
expect(client.post).toHaveBeenCalledWith('/security/headers/csp/validate', { csp });
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('buildCSP builds CSP', async () => {
|
||||
const directives = [{ directive: 'default-src', values: ["'self'"] }];
|
||||
const mockResponse = { csp: "default-src 'self'" };
|
||||
(client.post as any).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await securityHeadersApi.buildCSP(directives);
|
||||
expect(client.post).toHaveBeenCalledWith('/security/headers/csp/build', { directives });
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user