chore: git cache cleanup

This commit is contained in:
GitHub Actions
2026-03-04 18:34:39 +00:00
parent 7a8b0343e4
commit c32cce2a88
2001 changed files with 0 additions and 683185 deletions

View File

@@ -1,179 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { accessListsApi } from '../accessLists';
import client from '../client';
import type { AccessList } from '../accessLists';
// Mock the client module
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('accessListsApi', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('list', () => {
it('should fetch all access lists', async () => {
const mockLists: AccessList[] = [
{
id: 1,
uuid: 'test-uuid',
name: 'Test ACL',
description: 'Test description',
type: 'whitelist',
ip_rules: '[{"cidr":"192.168.1.0/24"}]',
country_codes: '',
local_network_only: false,
enabled: true,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
},
];
vi.mocked(client.get).mockResolvedValueOnce({ data: mockLists });
const result = await accessListsApi.list();
expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists');
expect(result).toEqual(mockLists);
});
});
describe('get', () => {
it('should fetch access list by ID', async () => {
const mockList: AccessList = {
id: 1,
uuid: 'test-uuid',
name: 'Test ACL',
description: 'Test description',
type: 'whitelist',
ip_rules: '[{"cidr":"192.168.1.0/24"}]',
country_codes: '',
local_network_only: false,
enabled: true,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
vi.mocked(client.get).mockResolvedValueOnce({ data: mockList });
const result = await accessListsApi.get(1);
expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists/1');
expect(result).toEqual(mockList);
});
});
describe('create', () => {
it('should create a new access list', async () => {
const newList = {
name: 'New ACL',
description: 'New description',
type: 'whitelist' as const,
ip_rules: '[{"cidr":"10.0.0.0/8"}]',
enabled: true,
};
const mockResponse: AccessList = {
id: 1,
uuid: 'new-uuid',
...newList,
country_codes: '',
local_network_only: false,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse });
const result = await accessListsApi.create(newList);
expect(client.post).toHaveBeenCalledWith<[string, typeof newList]>('/access-lists', newList);
expect(result).toEqual(mockResponse);
});
});
describe('update', () => {
it('should update an access list', async () => {
const updates = {
name: 'Updated ACL',
enabled: false,
};
const mockResponse: AccessList = {
id: 1,
uuid: 'test-uuid',
name: 'Updated ACL',
description: 'Test description',
type: 'whitelist',
ip_rules: '[{"cidr":"192.168.1.0/24"}]',
country_codes: '',
local_network_only: false,
enabled: false,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
vi.mocked(client.put).mockResolvedValueOnce({ data: mockResponse });
const result = await accessListsApi.update(1, updates);
expect(client.put).toHaveBeenCalledWith<[string, typeof updates]>('/access-lists/1', updates);
expect(result).toEqual(mockResponse);
});
});
describe('delete', () => {
it('should delete an access list', async () => {
vi.mocked(client.delete).mockResolvedValueOnce({ data: undefined });
await accessListsApi.delete(1);
expect(client.delete).toHaveBeenCalledWith<[string]>('/access-lists/1');
});
});
describe('testIP', () => {
it('should test an IP against an access list', async () => {
const mockResponse = {
allowed: true,
reason: 'IP matches whitelist rule',
};
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse });
const result = await accessListsApi.testIP(1, '192.168.1.100');
expect(client.post).toHaveBeenCalledWith<[string, { ip_address: string }]>('/access-lists/1/test', {
ip_address: '192.168.1.100',
});
expect(result).toEqual(mockResponse);
});
});
describe('getTemplates', () => {
it('should fetch access list templates', async () => {
const mockTemplates = [
{
name: 'Private Networks',
description: 'RFC1918 private networks',
type: 'whitelist' as const,
ip_rules: '[{"cidr":"10.0.0.0/8"},{"cidr":"172.16.0.0/12"},{"cidr":"192.168.0.0/16"}]',
},
];
vi.mocked(client.get).mockResolvedValueOnce({ data: mockTemplates });
const result = await accessListsApi.getTemplates();
expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists/templates');
expect(result).toEqual(mockTemplates);
});
});
});

View File

@@ -1,34 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import client from '../../api/client'
import { getBackups, createBackup, restoreBackup, deleteBackup } from '../backups'
describe('backups api', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('getBackups returns list', async () => {
const mockData = [{ filename: 'b1.zip', size: 123, time: '2025-01-01T00:00:00Z' }]
vi.spyOn(client, 'get').mockResolvedValueOnce({ data: mockData })
const res = await getBackups()
expect(res).toEqual(mockData)
})
it('createBackup returns filename', async () => {
vi.spyOn(client, 'post').mockResolvedValueOnce({ data: { filename: 'b2.zip' } })
const res = await createBackup()
expect(res).toEqual({ filename: 'b2.zip' })
})
it('restoreBackup posts to restore endpoint', async () => {
const spy = vi.spyOn(client, 'post').mockResolvedValueOnce({})
await restoreBackup('b3.zip')
expect(spy).toHaveBeenCalledWith('/backups/b3.zip/restore')
})
it('deleteBackup deletes backup', async () => {
const spy = vi.spyOn(client, 'delete').mockResolvedValueOnce({})
await deleteBackup('b3.zip')
expect(spy).toHaveBeenCalledWith('/backups/b3.zip')
})
})

View File

@@ -1,52 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import client from '../client';
import { getCertificates, uploadCertificate, deleteCertificate, Certificate } from '../certificates';
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}));
describe('certificates API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockCert: Certificate = {
id: 1,
domain: 'example.com',
issuer: 'Let\'s Encrypt',
expires_at: '2023-01-01',
status: 'valid',
provider: 'letsencrypt',
};
it('getCertificates calls client.get', async () => {
vi.mocked(client.get).mockResolvedValue({ data: [mockCert] });
const result = await getCertificates();
expect(client.get).toHaveBeenCalledWith('/certificates');
expect(result).toEqual([mockCert]);
});
it('uploadCertificate calls client.post with FormData', async () => {
vi.mocked(client.post).mockResolvedValue({ data: mockCert });
const certFile = new File(['cert'], 'cert.pem', { type: 'text/plain' });
const keyFile = new File(['key'], 'key.pem', { type: 'text/plain' });
const result = await uploadCertificate('My Cert', certFile, keyFile);
expect(client.post).toHaveBeenCalledWith('/certificates', expect.any(FormData), {
headers: { 'Content-Type': 'multipart/form-data' },
});
expect(result).toEqual(mockCert);
});
it('deleteCertificate calls client.delete', async () => {
vi.mocked(client.delete).mockResolvedValue({ data: {} });
await deleteCertificate(1);
expect(client.delete).toHaveBeenCalledWith('/certificates/1');
});
});

View File

@@ -1,205 +0,0 @@
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('keeps original message when response payload is not an object', async () => {
const error: ResponseError = {
response: { data: 'plain text error' as unknown as Record<string, unknown> },
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('Original')
})
it('uses error field over message field when both exist', async () => {
const error: ResponseError = {
response: { data: { error: 'Preferred error', message: 'Secondary message' } },
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('Preferred error')
})
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
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
await expect(resultPromise).rejects.toBe(error)
expect(onAuthError).not.toHaveBeenCalled()
warnSpy.mockRestore()
})
it('does not invoke auth error handler when status is not 401', async () => {
const onAuthError = vi.fn()
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
setAuthErrorHandler(onAuthError)
const error: ResponseError = {
response: { status: 403, data: { message: 'Forbidden' } },
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).not.toHaveBeenCalled()
expect(warnSpy).not.toHaveBeenCalled()
warnSpy.mockRestore()
})
it('passes through successful responses via fulfilled interceptor', () => {
const responsePayload = { data: { ok: true } }
const fulfilled = capturedHandlers.onFulfilled
expect(fulfilled).toBeDefined()
const result = fulfilled ? fulfilled(responsePayload) : undefined
expect(result).toBe(responsePayload)
})
})

View File

@@ -1,507 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as consoleEnrollment from '../consoleEnrollment'
import client from '../client'
vi.mock('../client')
describe('consoleEnrollment API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getConsoleStatus', () => {
it('should fetch enrollment status with pending state', async () => {
const mockStatus = {
status: 'pending',
tenant: 'my-org',
agent_name: 'charon-prod',
key_present: true,
last_attempt_at: '2025-12-15T09:00:00Z',
}
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
const result = await consoleEnrollment.getConsoleStatus()
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/console/status')
expect(result).toEqual(mockStatus)
expect(result.status).toBe('pending')
expect(result.key_present).toBe(true)
})
it('should fetch enrolled status with heartbeat', async () => {
const mockStatus = {
status: 'enrolled',
tenant: 'my-org',
agent_name: 'charon-prod',
key_present: true,
enrolled_at: '2025-12-14T10:00:00Z',
last_heartbeat_at: '2025-12-15T09:55:00Z',
}
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
const result = await consoleEnrollment.getConsoleStatus()
expect(result.status).toBe('enrolled')
expect(result.enrolled_at).toBeDefined()
expect(result.last_heartbeat_at).toBeDefined()
})
it('should fetch failed status with error message', async () => {
const mockStatus = {
status: 'failed',
tenant: 'my-org',
agent_name: 'charon-prod',
key_present: false,
last_error: 'Invalid enrollment key',
last_attempt_at: '2025-12-15T09:00:00Z',
correlation_id: 'req-abc123',
}
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
const result = await consoleEnrollment.getConsoleStatus()
expect(result.status).toBe('failed')
expect(result.last_error).toBe('Invalid enrollment key')
expect(result.correlation_id).toBe('req-abc123')
expect(result.key_present).toBe(false)
})
it('should fetch status with none state (not enrolled)', async () => {
const mockStatus = {
status: 'none',
key_present: false,
}
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
const result = await consoleEnrollment.getConsoleStatus()
expect(result.status).toBe('none')
expect(result.key_present).toBe(false)
expect(result.tenant).toBeUndefined()
})
it('should NOT return enrollment key in status response', async () => {
const mockStatus = {
status: 'enrolled',
tenant: 'test-org',
agent_name: 'test-agent',
key_present: true,
enrolled_at: '2025-12-14T10:00:00Z',
}
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
const result = await consoleEnrollment.getConsoleStatus()
// Security test: Ensure key is never exposed
expect(result).not.toHaveProperty('enrollment_key')
expect(result).not.toHaveProperty('encrypted_enroll_key')
expect(result).toHaveProperty('key_present')
})
it('should handle API errors', async () => {
const error = new Error('Network error')
vi.mocked(client.get).mockRejectedValue(error)
await expect(consoleEnrollment.getConsoleStatus()).rejects.toThrow('Network error')
})
it('should handle server unavailability', async () => {
const error = {
response: {
status: 503,
data: { error: 'Service temporarily unavailable' },
},
}
vi.mocked(client.get).mockRejectedValue(error)
await expect(consoleEnrollment.getConsoleStatus()).rejects.toEqual(error)
})
})
describe('enrollConsole', () => {
it('should enroll with valid payload', async () => {
const payload = {
enrollment_key: 'cs-enroll-abc123xyz',
tenant: 'my-org',
agent_name: 'charon-prod',
force: false,
}
const mockResponse = {
status: 'enrolled',
tenant: 'my-org',
agent_name: 'charon-prod',
key_present: true,
enrolled_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await consoleEnrollment.enrollConsole(payload)
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/console/enroll', payload)
expect(result).toEqual(mockResponse)
expect(result.status).toBe('enrolled')
expect(result.enrolled_at).toBeDefined()
})
it('should enroll with minimal payload (no tenant)', async () => {
const payload = {
enrollment_key: 'cs-enroll-key123',
agent_name: 'charon-test',
}
const mockResponse = {
status: 'enrolled',
agent_name: 'charon-test',
key_present: true,
enrolled_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await consoleEnrollment.enrollConsole(payload)
expect(result.status).toBe('enrolled')
expect(result.agent_name).toBe('charon-test')
})
it('should force re-enrollment when force=true', async () => {
const payload = {
enrollment_key: 'cs-enroll-new-key',
agent_name: 'charon-updated',
force: true,
}
const mockResponse = {
status: 'enrolled',
agent_name: 'charon-updated',
key_present: true,
enrolled_at: '2025-12-15T10:05:00Z',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await consoleEnrollment.enrollConsole(payload)
expect(result.status).toBe('enrolled')
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/console/enroll', payload)
})
it('should handle invalid enrollment key format', async () => {
const payload = {
enrollment_key: 'not-a-valid-key',
agent_name: 'test',
}
const error = {
response: {
status: 400,
data: { error: 'Invalid enrollment key format' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
})
it('should handle transient network errors during enrollment', async () => {
const payload = {
enrollment_key: 'cs-enroll-key123',
agent_name: 'test-agent',
}
const error = {
response: {
status: 503,
data: { error: 'CrowdSec Console API temporarily unavailable' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
})
it('should handle enrollment key expiration', async () => {
const payload = {
enrollment_key: 'cs-enroll-expired-key',
agent_name: 'test',
}
const mockResponse = {
status: 'failed',
key_present: false,
last_error: 'Enrollment key expired',
correlation_id: 'err-expired-123',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await consoleEnrollment.enrollConsole(payload)
expect(result.status).toBe('failed')
expect(result.last_error).toBe('Enrollment key expired')
})
it('should sanitize tenant name with special characters', async () => {
const payload = {
enrollment_key: 'valid-key',
tenant: 'My Org (Production)',
agent_name: 'agent1',
}
const mockResponse = {
status: 'enrolled',
tenant: 'My Org (Production)',
agent_name: 'agent1',
key_present: true,
enrolled_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await consoleEnrollment.enrollConsole(payload)
expect(result.status).toBe('enrolled')
expect(result.tenant).toBe('My Org (Production)')
})
it('should handle SQL injection attempts in agent_name', async () => {
const payload = {
enrollment_key: 'valid-key',
agent_name: "'; DROP TABLE users; --",
}
const error = {
response: {
status: 400,
data: { error: 'Invalid agent name format' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
})
it('should handle CrowdSec not running during enrollment', async () => {
const payload = {
enrollment_key: 'valid-key',
agent_name: 'test',
}
const error = {
response: {
status: 500,
data: { error: 'CrowdSec is not running. Start CrowdSec before enrolling.' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
})
it('should return pending status when enrollment is queued', async () => {
const payload = {
enrollment_key: 'valid-key',
agent_name: 'test',
}
const mockResponse = {
status: 'pending',
agent_name: 'test',
key_present: true,
last_attempt_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await consoleEnrollment.enrollConsole(payload)
expect(result.status).toBe('pending')
expect(result.last_attempt_at).toBeDefined()
})
})
describe('default export', () => {
it('should export all functions', () => {
expect(consoleEnrollment.default).toHaveProperty('getConsoleStatus')
expect(consoleEnrollment.default).toHaveProperty('enrollConsole')
})
})
describe('integration scenarios', () => {
it('should handle full enrollment workflow: status → enroll → verify', async () => {
// 1. Check initial status (not enrolled)
const mockStatusNone = {
status: 'none',
key_present: false,
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockStatusNone })
const statusBefore = await consoleEnrollment.getConsoleStatus()
expect(statusBefore.status).toBe('none')
// 2. Enroll
const enrollPayload = {
enrollment_key: 'cs-enroll-valid-key',
tenant: 'test-org',
agent_name: 'charon-test',
}
const mockEnrollResponse = {
status: 'enrolled',
tenant: 'test-org',
agent_name: 'charon-test',
key_present: true,
enrolled_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockEnrollResponse })
const enrollResult = await consoleEnrollment.enrollConsole(enrollPayload)
expect(enrollResult.status).toBe('enrolled')
// 3. Verify status updated
const mockStatusEnrolled = {
status: 'enrolled',
tenant: 'test-org',
agent_name: 'charon-test',
key_present: true,
enrolled_at: '2025-12-15T10:00:00Z',
last_heartbeat_at: '2025-12-15T10:01:00Z',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockStatusEnrolled })
const statusAfter = await consoleEnrollment.getConsoleStatus()
expect(statusAfter.status).toBe('enrolled')
expect(statusAfter.tenant).toBe('test-org')
})
it('should handle enrollment failure and retry', async () => {
// 1. First enrollment attempt fails
const payload = {
enrollment_key: 'cs-enroll-key',
agent_name: 'test',
}
const networkError = new Error('Network timeout')
vi.mocked(client.post).mockRejectedValueOnce(networkError)
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toThrow('Network timeout')
// 2. Retry succeeds
const mockResponse = {
status: 'enrolled',
agent_name: 'test',
key_present: true,
enrolled_at: '2025-12-15T10:05:00Z',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse })
const retryResult = await consoleEnrollment.enrollConsole(payload)
expect(retryResult.status).toBe('enrolled')
})
it('should handle status transitions: none → pending → enrolled', async () => {
// 1. Initial: none
const mockNone = { status: 'none', key_present: false }
vi.mocked(client.get).mockResolvedValueOnce({ data: mockNone })
const status1 = await consoleEnrollment.getConsoleStatus()
expect(status1.status).toBe('none')
// 2. Enroll (returns pending)
const payload = { enrollment_key: 'key', agent_name: 'agent' }
const mockPending = {
status: 'pending',
agent_name: 'agent',
key_present: true,
last_attempt_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockPending })
const enrollResult = await consoleEnrollment.enrollConsole(payload)
expect(enrollResult.status).toBe('pending')
// 3. Check status again (now enrolled)
const mockEnrolled = {
status: 'enrolled',
agent_name: 'agent',
key_present: true,
enrolled_at: '2025-12-15T10:00:30Z',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockEnrolled })
const status2 = await consoleEnrollment.getConsoleStatus()
expect(status2.status).toBe('enrolled')
})
it('should handle force re-enrollment over existing enrollment', async () => {
// 1. Check current enrollment
const mockCurrent = {
status: 'enrolled',
tenant: 'old-org',
agent_name: 'old-agent',
key_present: true,
enrolled_at: '2025-12-14T10:00:00Z',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockCurrent })
const currentStatus = await consoleEnrollment.getConsoleStatus()
expect(currentStatus.tenant).toBe('old-org')
// 2. Force re-enrollment
const forcePayload = {
enrollment_key: 'new-key',
tenant: 'new-org',
agent_name: 'new-agent',
force: true,
}
const mockForced = {
status: 'enrolled',
tenant: 'new-org',
agent_name: 'new-agent',
key_present: true,
enrolled_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockForced })
const forceResult = await consoleEnrollment.enrollConsole(forcePayload)
expect(forceResult.tenant).toBe('new-org')
})
})
describe('security tests', () => {
it('should never log or expose enrollment key', async () => {
const payload = {
enrollment_key: 'cs-enroll-secret-key-should-never-log',
agent_name: 'test',
}
const mockResponse = {
status: 'enrolled',
agent_name: 'test',
key_present: true,
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await consoleEnrollment.enrollConsole(payload)
// Ensure response never contains the key
expect(result).not.toHaveProperty('enrollment_key')
expect(JSON.stringify(result)).not.toContain('cs-enroll-secret-key')
})
it('should sanitize error messages to avoid key leakage', async () => {
const payload = {
enrollment_key: 'cs-enroll-sensitive-key',
agent_name: 'test',
}
const error = {
response: {
status: 400,
data: { error: 'Enrollment failed: invalid key format' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
try {
await consoleEnrollment.enrollConsole(payload)
} catch (e: unknown) {
// Error message should NOT contain the key
const error = e as { response?: { data?: { error?: string } } }
expect(error.response?.data?.error).not.toContain('cs-enroll-sensitive-key')
}
})
it('should handle correlation_id for debugging without exposing keys', async () => {
const mockStatus = {
status: 'failed',
key_present: false,
last_error: 'Authentication failed',
correlation_id: 'debug-correlation-abc123',
}
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
const result = await consoleEnrollment.getConsoleStatus()
expect(result.correlation_id).toBe('debug-correlation-abc123')
expect(result).not.toHaveProperty('enrollment_key')
})
})
})

View File

@@ -1,119 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
getCredentials,
getCredential,
createCredential,
updateCredential,
deleteCredential,
testCredential,
enableMultiCredentials,
type DNSProviderCredential,
type CredentialRequest,
type CredentialTestResult,
} from '../credentials'
import client from '../client'
vi.mock('../client')
const mockCredential: DNSProviderCredential = {
id: 1,
uuid: 'test-uuid-1',
dns_provider_id: 1,
label: 'Production Credentials',
zone_filter: '*.example.com',
enabled: true,
propagation_timeout: 120,
polling_interval: 2,
key_version: 1,
success_count: 5,
failure_count: 0,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
const mockCredentialRequest: CredentialRequest = {
label: 'New Credentials',
zone_filter: '*.example.com',
credentials: { api_token: 'test-token-123' },
propagation_timeout: 120,
polling_interval: 2,
enabled: true,
}
describe('credentials API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should call getCredentials with correct endpoint', async () => {
const mockData = [mockCredential, { ...mockCredential, id: 2, label: 'Secondary' }]
vi.mocked(client.get).mockResolvedValue({
data: { credentials: mockData, total: 2 },
})
const result = await getCredentials(1)
expect(client.get).toHaveBeenCalledWith('/dns-providers/1/credentials')
expect(result).toEqual(mockData)
expect(result).toHaveLength(2)
})
it('should call getCredential with correct endpoint', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockCredential })
const result = await getCredential(1, 1)
expect(client.get).toHaveBeenCalledWith('/dns-providers/1/credentials/1')
expect(result).toEqual(mockCredential)
})
it('should call createCredential with correct endpoint and data', async () => {
vi.mocked(client.post).mockResolvedValue({ data: mockCredential })
const result = await createCredential(1, mockCredentialRequest)
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/credentials', mockCredentialRequest)
expect(result).toEqual(mockCredential)
})
it('should call updateCredential with correct endpoint and data', async () => {
const updatedCredential = { ...mockCredential, label: 'Updated Label' }
vi.mocked(client.put).mockResolvedValue({ data: updatedCredential })
const result = await updateCredential(1, 1, mockCredentialRequest)
expect(client.put).toHaveBeenCalledWith('/dns-providers/1/credentials/1', mockCredentialRequest)
expect(result).toEqual(updatedCredential)
})
it('should call deleteCredential with correct endpoint', async () => {
vi.mocked(client.delete).mockResolvedValue({ data: undefined })
await deleteCredential(1, 1)
expect(client.delete).toHaveBeenCalledWith('/dns-providers/1/credentials/1')
})
it('should call testCredential with correct endpoint', async () => {
const mockTestResult: CredentialTestResult = {
success: true,
message: 'Credentials validated successfully',
propagation_time_ms: 1200,
}
vi.mocked(client.post).mockResolvedValue({ data: mockTestResult })
const result = await testCredential(1, 1)
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/credentials/1/test')
expect(result).toEqual(mockTestResult)
expect(result.success).toBe(true)
})
it('should call enableMultiCredentials with correct endpoint', async () => {
vi.mocked(client.post).mockResolvedValue({ data: undefined })
await enableMultiCredentials(1)
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/enable-multi-credentials')
})
})

View File

@@ -1,130 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as crowdsec from '../crowdsec'
import client from '../client'
vi.mock('../client')
describe('crowdsec API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('startCrowdsec', () => {
it('should call POST /admin/crowdsec/start', async () => {
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await crowdsec.startCrowdsec()
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/start')
expect(result).toEqual(mockData)
})
})
describe('stopCrowdsec', () => {
it('should call POST /admin/crowdsec/stop', async () => {
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await crowdsec.stopCrowdsec()
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/stop')
expect(result).toEqual(mockData)
})
})
describe('statusCrowdsec', () => {
it('should call GET /admin/crowdsec/status', async () => {
const mockData = { running: true, pid: 1234 }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await crowdsec.statusCrowdsec()
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/status')
expect(result).toEqual(mockData)
})
})
describe('importCrowdsecConfig', () => {
it('should call POST /admin/crowdsec/import with FormData', async () => {
const mockFile = new File(['content'], 'config.tar.gz', { type: 'application/gzip' })
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await crowdsec.importCrowdsecConfig(mockFile)
expect(client.post).toHaveBeenCalledWith(
'/admin/crowdsec/import',
expect.any(FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } }
)
expect(result).toEqual(mockData)
})
})
describe('exportCrowdsecConfig', () => {
it('should call GET /admin/crowdsec/export with blob responseType', async () => {
const mockBlob = new Blob(['data'], { type: 'application/gzip' })
vi.mocked(client.get).mockResolvedValue({ data: mockBlob })
const result = await crowdsec.exportCrowdsecConfig()
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/export', { responseType: 'blob' })
expect(result).toEqual(mockBlob)
})
})
describe('listCrowdsecFiles', () => {
it('should call GET /admin/crowdsec/files', async () => {
const mockData = { files: ['file1.yaml', 'file2.yaml'] }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await crowdsec.listCrowdsecFiles()
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/files')
expect(result).toEqual(mockData)
})
})
describe('readCrowdsecFile', () => {
it('should call GET /admin/crowdsec/file with encoded path', async () => {
const mockData = { content: 'file content' }
const path = '/etc/crowdsec/file.yaml'
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await crowdsec.readCrowdsecFile(path)
expect(client.get).toHaveBeenCalledWith(
`/admin/crowdsec/file?path=${encodeURIComponent(path)}`
)
expect(result).toEqual(mockData)
})
})
describe('writeCrowdsecFile', () => {
it('should call POST /admin/crowdsec/file with path and content', async () => {
const mockData = { success: true }
const path = '/etc/crowdsec/file.yaml'
const content = 'new content'
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await crowdsec.writeCrowdsecFile(path, content)
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/file', { path, content })
expect(result).toEqual(mockData)
})
})
describe('default export', () => {
it('should export all functions', () => {
expect(crowdsec.default).toHaveProperty('startCrowdsec')
expect(crowdsec.default).toHaveProperty('stopCrowdsec')
expect(crowdsec.default).toHaveProperty('statusCrowdsec')
expect(crowdsec.default).toHaveProperty('importCrowdsecConfig')
expect(crowdsec.default).toHaveProperty('exportCrowdsecConfig')
expect(crowdsec.default).toHaveProperty('listCrowdsecFiles')
expect(crowdsec.default).toHaveProperty('readCrowdsecFile')
expect(crowdsec.default).toHaveProperty('writeCrowdsecFile')
})
})
})

View File

@@ -1,138 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { detectDNSProvider, getDetectionPatterns } from '../dnsDetection'
import client from '../client'
import type { DetectionResult, NameserverPattern } from '../dnsDetection'
vi.mock('../client')
describe('dnsDetection API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('detectDNSProvider', () => {
it('should detect DNS provider successfully', async () => {
const mockResponse: DetectionResult = {
domain: 'example.com',
detected: true,
provider_type: 'cloudflare',
nameservers: ['ns1.cloudflare.com', 'ns2.cloudflare.com'],
confidence: 'high',
suggested_provider: {
id: 1,
uuid: 'test-uuid',
name: 'Production Cloudflare',
provider_type: 'cloudflare',
enabled: true,
is_default: true,
has_credentials: true,
propagation_timeout: 120,
polling_interval: 5,
success_count: 10,
failure_count: 0,
created_at: '2026-01-01T00:00:00Z',
updated_at: '2026-01-01T00:00:00Z',
},
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await detectDNSProvider('example.com')
expect(client.post).toHaveBeenCalledWith('/dns-providers/detect', { domain: 'example.com' })
expect(result).toEqual(mockResponse)
expect(result.detected).toBe(true)
expect(result.provider_type).toBe('cloudflare')
expect(result.confidence).toBe('high')
})
it('should handle detection failure (no provider found)', async () => {
const mockResponse: DetectionResult = {
domain: 'example.com',
detected: false,
nameservers: ['ns1.unknown.com', 'ns2.unknown.com'],
confidence: 'none',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await detectDNSProvider('example.com')
expect(result.detected).toBe(false)
expect(result.confidence).toBe('none')
expect(result.nameservers).toHaveLength(2)
})
it('should handle detection error', async () => {
const mockResponse: DetectionResult = {
domain: 'invalid.domain',
detected: false,
nameservers: [],
confidence: 'none',
error: 'Failed to lookup nameservers: domain not found',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await detectDNSProvider('invalid.domain')
expect(result.detected).toBe(false)
expect(result.error).toContain('domain not found')
})
it('should handle network error', async () => {
vi.mocked(client.post).mockRejectedValue(new Error('Network error'))
await expect(detectDNSProvider('example.com')).rejects.toThrow('Network error')
})
it('should handle medium confidence detection', async () => {
const mockResponse: DetectionResult = {
domain: 'example.com',
detected: true,
provider_type: 'route53',
nameservers: ['ns-123.awsdns-12.com'],
confidence: 'medium',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await detectDNSProvider('example.com')
expect(result.confidence).toBe('medium')
expect(result.detected).toBe(true)
})
})
describe('getDetectionPatterns', () => {
it('should fetch detection patterns successfully', async () => {
const mockPatterns: NameserverPattern[] = [
{ pattern: '.ns.cloudflare.com', provider_type: 'cloudflare' },
{ pattern: '.awsdns', provider_type: 'route53' },
{ pattern: '.digitalocean.com', provider_type: 'digitalocean' },
]
vi.mocked(client.get).mockResolvedValue({ data: { patterns: mockPatterns } })
const result = await getDetectionPatterns()
expect(client.get).toHaveBeenCalledWith('/dns-providers/patterns')
expect(result).toEqual(mockPatterns)
expect(result).toHaveLength(3)
})
it('should handle empty patterns list', async () => {
vi.mocked(client.get).mockResolvedValue({ data: { patterns: [] } })
const result = await getDetectionPatterns()
expect(result).toEqual([])
})
it('should handle network error when fetching patterns', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Network error'))
await expect(getDetectionPatterns()).rejects.toThrow('Network error')
})
})
})

View File

@@ -1,431 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
getDNSProviders,
getDNSProvider,
getDNSProviderTypes,
createDNSProvider,
updateDNSProvider,
deleteDNSProvider,
testDNSProvider,
testDNSProviderCredentials,
type DNSProvider,
type DNSProviderRequest,
type DNSProviderTypeInfo,
} from '../dnsProviders'
import client from '../client'
vi.mock('../client')
const mockProvider: DNSProvider = {
id: 1,
uuid: 'test-uuid-1',
name: 'Cloudflare Production',
provider_type: 'cloudflare',
enabled: true,
is_default: true,
has_credentials: true,
propagation_timeout: 120,
polling_interval: 2,
success_count: 5,
failure_count: 0,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
const mockProviderType: DNSProviderTypeInfo = {
type: 'cloudflare',
name: 'Cloudflare',
fields: [
{
name: 'api_token',
label: 'API Token',
type: 'password',
required: true,
hint: 'Cloudflare API token with DNS edit permissions',
},
],
documentation_url: 'https://developers.cloudflare.com/api/',
}
describe('getDNSProviders', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches all DNS providers successfully', async () => {
const mockProviders = [mockProvider, { ...mockProvider, id: 2, name: 'Secondary' }]
vi.mocked(client.get).mockResolvedValue({
data: { providers: mockProviders, total: 2 },
})
const result = await getDNSProviders()
expect(client.get).toHaveBeenCalledWith('/dns-providers')
expect(result).toEqual(mockProviders)
expect(result).toHaveLength(2)
})
it('returns empty array when no providers exist', async () => {
vi.mocked(client.get).mockResolvedValue({
data: { providers: [], total: 0 },
})
const result = await getDNSProviders()
expect(result).toEqual([])
})
it('handles network errors', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Network error'))
await expect(getDNSProviders()).rejects.toThrow('Network error')
})
it('handles server errors', async () => {
vi.mocked(client.get).mockRejectedValue({ response: { status: 500 } })
await expect(getDNSProviders()).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('getDNSProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches single provider by valid ID', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockProvider })
const result = await getDNSProvider(1)
expect(client.get).toHaveBeenCalledWith('/dns-providers/1')
expect(result).toEqual(mockProvider)
})
it('handles not found error for invalid ID', async () => {
vi.mocked(client.get).mockRejectedValue({ response: { status: 404 } })
await expect(getDNSProvider(999)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles server errors', async () => {
vi.mocked(client.get).mockRejectedValue({ response: { status: 500 } })
await expect(getDNSProvider(1)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('getDNSProviderTypes', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches supported provider types with field definitions', async () => {
const mockTypes = [
mockProviderType,
{
type: 'route53',
name: 'AWS Route 53',
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 },
],
documentation_url: 'https://aws.amazon.com/route53/',
} as DNSProviderTypeInfo,
]
vi.mocked(client.get).mockResolvedValue({
data: { types: mockTypes },
})
const result = await getDNSProviderTypes()
expect(client.get).toHaveBeenCalledWith('/dns-providers/types')
expect(result).toEqual(mockTypes)
expect(result).toHaveLength(2)
})
it('handles errors when fetching types', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Failed to fetch types'))
await expect(getDNSProviderTypes()).rejects.toThrow('Failed to fetch types')
})
})
describe('createDNSProvider', () => {
const validRequest: DNSProviderRequest = {
name: 'New Cloudflare',
provider_type: 'cloudflare',
credentials: { api_token: 'test-token-123' },
propagation_timeout: 120,
polling_interval: 2,
}
beforeEach(() => {
vi.clearAllMocks()
})
it('creates provider successfully and returns with ID', async () => {
const createdProvider = { ...mockProvider, id: 5, name: 'New Cloudflare' }
vi.mocked(client.post).mockResolvedValue({ data: createdProvider })
const result = await createDNSProvider(validRequest)
expect(client.post).toHaveBeenCalledWith('/dns-providers', validRequest)
expect(result).toEqual(createdProvider)
expect(result.id).toBe(5)
})
it('handles validation error for missing required fields', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 400, data: { error: 'Missing required field: api_token' } },
})
await expect(
createDNSProvider({ ...validRequest, credentials: {} })
).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles validation error for invalid provider type', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 400, data: { error: 'Invalid provider type' } },
})
await expect(
createDNSProvider({ ...validRequest, provider_type: 'invalid' as DNSProviderRequest['provider_type'] })
).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles duplicate name error', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 409, data: { error: 'Provider with this name already exists' } },
})
await expect(createDNSProvider(validRequest)).rejects.toMatchObject({
response: { status: 409 },
})
})
it('handles server errors', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
await expect(createDNSProvider(validRequest)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('updateDNSProvider', () => {
const updateRequest: DNSProviderRequest = {
name: 'Updated Name',
provider_type: 'cloudflare',
credentials: { api_token: 'new-token' },
}
beforeEach(() => {
vi.clearAllMocks()
})
it('updates provider successfully', async () => {
const updatedProvider = { ...mockProvider, name: 'Updated Name' }
vi.mocked(client.put).mockResolvedValue({ data: updatedProvider })
const result = await updateDNSProvider(1, updateRequest)
expect(client.put).toHaveBeenCalledWith('/dns-providers/1', updateRequest)
expect(result).toEqual(updatedProvider)
expect(result.name).toBe('Updated Name')
})
it('handles not found error', async () => {
vi.mocked(client.put).mockRejectedValue({ response: { status: 404 } })
await expect(updateDNSProvider(999, updateRequest)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles validation errors', async () => {
vi.mocked(client.put).mockRejectedValue({
response: { status: 400, data: { error: 'Invalid credentials' } },
})
await expect(updateDNSProvider(1, updateRequest)).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles server errors', async () => {
vi.mocked(client.put).mockRejectedValue({ response: { status: 500 } })
await expect(updateDNSProvider(1, updateRequest)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('deleteDNSProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('deletes provider successfully', async () => {
vi.mocked(client.delete).mockResolvedValue({ data: undefined })
await deleteDNSProvider(1)
expect(client.delete).toHaveBeenCalledWith('/dns-providers/1')
})
it('handles not found error', async () => {
vi.mocked(client.delete).mockRejectedValue({ response: { status: 404 } })
await expect(deleteDNSProvider(999)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles in-use error when provider used by proxy hosts', async () => {
vi.mocked(client.delete).mockRejectedValue({
response: {
status: 409,
data: { error: 'Cannot delete provider in use by proxy hosts' },
},
})
await expect(deleteDNSProvider(1)).rejects.toMatchObject({
response: { status: 409 },
})
})
it('handles server errors', async () => {
vi.mocked(client.delete).mockRejectedValue({ response: { status: 500 } })
await expect(deleteDNSProvider(1)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('testDNSProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns success result with propagation time', async () => {
const successResult = {
success: true,
message: 'DNS challenge completed successfully',
propagation_time_ms: 1500,
}
vi.mocked(client.post).mockResolvedValue({ data: successResult })
const result = await testDNSProvider(1)
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/test')
expect(result).toEqual(successResult)
expect(result.success).toBe(true)
expect(result.propagation_time_ms).toBe(1500)
})
it('returns failure result with error message', async () => {
const failureResult = {
success: false,
error: 'Invalid API token',
code: 'AUTH_FAILED',
}
vi.mocked(client.post).mockResolvedValue({ data: failureResult })
const result = await testDNSProvider(1)
expect(result).toEqual(failureResult)
expect(result.success).toBe(false)
expect(result.error).toBe('Invalid API token')
})
it('handles not found error', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 404 } })
await expect(testDNSProvider(999)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles server errors', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
await expect(testDNSProvider(1)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('testDNSProviderCredentials', () => {
const testRequest: DNSProviderRequest = {
name: 'Test Provider',
provider_type: 'cloudflare',
credentials: { api_token: 'test-token' },
}
beforeEach(() => {
vi.clearAllMocks()
})
it('returns success for valid credentials', async () => {
const successResult = {
success: true,
message: 'Credentials validated successfully',
propagation_time_ms: 800,
}
vi.mocked(client.post).mockResolvedValue({ data: successResult })
const result = await testDNSProviderCredentials(testRequest)
expect(client.post).toHaveBeenCalledWith('/dns-providers/test', testRequest)
expect(result).toEqual(successResult)
expect(result.success).toBe(true)
})
it('returns failure for invalid credentials', async () => {
const failureResult = {
success: false,
error: 'Authentication failed',
code: 'INVALID_CREDENTIALS',
}
vi.mocked(client.post).mockResolvedValue({ data: failureResult })
const result = await testDNSProviderCredentials(testRequest)
expect(result).toEqual(failureResult)
expect(result.success).toBe(false)
})
it('handles validation errors for missing credentials', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 400, data: { error: 'Missing required field: api_token' } },
})
await expect(
testDNSProviderCredentials({ ...testRequest, credentials: {} })
).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles server errors', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
await expect(testDNSProviderCredentials(testRequest)).rejects.toMatchObject({
response: { status: 500 },
})
})
})

View File

@@ -1,96 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { dockerApi } from '../docker';
import client from '../client';
vi.mock('../client', () => ({
default: {
get: vi.fn(),
},
}));
describe('dockerApi', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('listContainers', () => {
const mockContainers = [
{
id: 'abc123',
names: ['/container1'],
image: 'nginx:latest',
state: 'running',
status: 'Up 2 hours',
network: 'bridge',
ip: '172.17.0.2',
ports: [{ private_port: 80, public_port: 8080, type: 'tcp' }],
},
{
id: 'def456',
names: ['/container2'],
image: 'redis:alpine',
state: 'running',
status: 'Up 1 hour',
network: 'bridge',
ip: '172.17.0.3',
ports: [],
},
];
it('fetches containers without parameters', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
const result = await dockerApi.listContainers();
expect(client.get).toHaveBeenCalledWith('/docker/containers', { params: {} });
expect(result).toEqual(mockContainers);
});
it('fetches containers with host parameter', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
const result = await dockerApi.listContainers('192.168.1.100');
expect(client.get).toHaveBeenCalledWith('/docker/containers', {
params: { host: '192.168.1.100' },
});
expect(result).toEqual(mockContainers);
});
it('fetches containers with serverId parameter', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
const result = await dockerApi.listContainers(undefined, 'server-uuid-123');
expect(client.get).toHaveBeenCalledWith('/docker/containers', {
params: { server_id: 'server-uuid-123' },
});
expect(result).toEqual(mockContainers);
});
it('fetches containers with both host and serverId parameters', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
const result = await dockerApi.listContainers('192.168.1.100', 'server-uuid-123');
expect(client.get).toHaveBeenCalledWith('/docker/containers', {
params: { host: '192.168.1.100', server_id: 'server-uuid-123' },
});
expect(result).toEqual(mockContainers);
});
it('returns empty array when no containers', async () => {
vi.mocked(client.get).mockResolvedValue({ data: [] });
const result = await dockerApi.listContainers();
expect(result).toEqual([]);
});
it('handles API error', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Network error'));
await expect(dockerApi.listContainers()).rejects.toThrow('Network error');
});
});
});

View File

@@ -1,44 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import client from '../client';
import { getDomains, createDomain, deleteDomain, Domain } from '../domains';
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}));
describe('domains API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockDomain: Domain = {
id: 1,
uuid: '123',
name: 'example.com',
created_at: '2023-01-01',
};
it('getDomains calls client.get', async () => {
vi.mocked(client.get).mockResolvedValue({ data: [mockDomain] });
const result = await getDomains();
expect(client.get).toHaveBeenCalledWith('/domains');
expect(result).toEqual([mockDomain]);
});
it('createDomain calls client.post', async () => {
vi.mocked(client.post).mockResolvedValue({ data: mockDomain });
const result = await createDomain('example.com');
expect(client.post).toHaveBeenCalledWith('/domains', { name: 'example.com' });
expect(result).toEqual(mockDomain);
});
it('deleteDomain calls client.delete', async () => {
vi.mocked(client.delete).mockResolvedValue({ data: {} });
await deleteDomain('123');
expect(client.delete).toHaveBeenCalledWith('/domains/123');
});
});

View File

@@ -1,95 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
getEncryptionStatus,
rotateEncryptionKey,
getRotationHistory,
validateKeyConfiguration,
type RotationStatus,
type RotationResult,
type RotationHistoryEntry,
type KeyValidationResult,
} from '../encryption'
import client from '../client'
vi.mock('../client')
const mockRotationStatus: RotationStatus = {
current_version: 2,
next_key_configured: true,
legacy_key_count: 1,
providers_on_current_version: 5,
providers_on_older_versions: 0,
}
const mockRotationResult: RotationResult = {
total_providers: 5,
success_count: 5,
failure_count: 0,
duration: '2.5s',
new_key_version: 3,
}
const mockHistoryEntry: RotationHistoryEntry = {
id: 1,
uuid: 'test-uuid-1',
actor: 'admin@example.com',
action: 'encryption_key_rotated',
event_category: 'security',
details: 'Rotated from version 1 to version 2',
created_at: '2025-01-01T00:00:00Z',
}
const mockValidationResult: KeyValidationResult = {
valid: true,
message: 'Key configuration is valid',
}
describe('encryption API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should call getEncryptionStatus with correct endpoint', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockRotationStatus })
const result = await getEncryptionStatus()
expect(client.get).toHaveBeenCalledWith('/admin/encryption/status')
expect(result).toEqual(mockRotationStatus)
expect(result.current_version).toBe(2)
})
it('should call rotateEncryptionKey with correct endpoint', async () => {
vi.mocked(client.post).mockResolvedValue({ data: mockRotationResult })
const result = await rotateEncryptionKey()
expect(client.post).toHaveBeenCalledWith('/admin/encryption/rotate')
expect(result).toEqual(mockRotationResult)
expect(result.new_key_version).toBe(3)
expect(result.success_count).toBe(5)
})
it('should call getRotationHistory with correct endpoint', async () => {
const mockHistory = [mockHistoryEntry, { ...mockHistoryEntry, id: 2 }]
vi.mocked(client.get).mockResolvedValue({
data: { history: mockHistory, total: 2 },
})
const result = await getRotationHistory()
expect(client.get).toHaveBeenCalledWith('/admin/encryption/history')
expect(result).toEqual(mockHistory)
expect(result).toHaveLength(2)
})
it('should call validateKeyConfiguration with correct endpoint', async () => {
vi.mocked(client.post).mockResolvedValue({ data: mockValidationResult })
const result = await validateKeyConfiguration()
expect(client.post).toHaveBeenCalledWith('/admin/encryption/validate')
expect(result).toEqual(mockValidationResult)
expect(result.valid).toBe(true)
})
})

View File

@@ -1,133 +0,0 @@
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(),
delete: vi.fn(),
},
}));
describe('import API', () => {
const mockedGet = vi.mocked(client.get);
const mockedPost = vi.mocked(client.post);
const mockedDelete = vi.mocked(client.delete);
beforeEach(() => {
vi.clearAllMocks();
});
it('uploadCaddyfile posts content', async () => {
const content = 'example.com';
const mockResponse = { preview: { hosts: [] } };
mockedPost.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: [] } };
mockedPost.mockResolvedValue({ data: mockResponse });
const result = await uploadCaddyfilesMulti(files);
expect(client.post).toHaveBeenCalledWith('/import/upload-multi', { files });
expect(result).toEqual(mockResponse);
});
it('uploadCaddyfilesMulti accepts empty file arrays', async () => {
mockedPost.mockResolvedValue({ data: { preview: { hosts: [], conflicts: [], errors: [] } } });
const result = await uploadCaddyfilesMulti([]);
expect(client.post).toHaveBeenCalledWith('/import/upload-multi', { files: [] });
expect(result).toEqual({ preview: { hosts: [], conflicts: [], errors: [] } });
});
it('getImportPreview gets preview', async () => {
const mockResponse = { preview: { hosts: [] } };
mockedGet.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: [] };
mockedPost.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 deletes cancel with required session_uuid query', async () => {
const sessionUUID = 'uuid-cancel-123';
mockedDelete.mockResolvedValue({});
await cancelImport(sessionUUID);
expect(client.delete).toHaveBeenCalledTimes(1);
expect(client.delete).toHaveBeenCalledWith('/import/cancel', {
params: {
session_uuid: sessionUUID,
},
});
const [, requestConfig] = mockedDelete.mock.calls[0];
expect(requestConfig).toEqual({
params: {
session_uuid: sessionUUID,
},
});
});
it('forwards commitImport errors', async () => {
const error = new Error('commit failed');
mockedPost.mockRejectedValue(error);
await expect(commitImport('uuid-123', {}, {})).rejects.toBe(error);
});
it('forwards cancelImport errors', async () => {
const error = new Error('cancel failed');
mockedDelete.mockRejectedValue(error);
await expect(cancelImport('uuid-cancel-123')).rejects.toBe(error);
});
it('getImportStatus gets status', async () => {
const mockResponse = { has_pending: true };
mockedGet.mockResolvedValue({ data: mockResponse });
const result = await getImportStatus();
expect(client.get).toHaveBeenCalledWith('/import/status');
expect(result).toEqual(mockResponse);
});
it('getImportStatus handles error', async () => {
mockedGet.mockRejectedValue(new Error('Failed'));
const result = await getImportStatus();
expect(result).toEqual({ has_pending: false });
});
it('getImportStatus returns false on non-Error rejections', async () => {
mockedGet.mockRejectedValue('network down');
const result = await getImportStatus();
expect(result).toEqual({ has_pending: false });
});
});

View File

@@ -1,96 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { uploadJSONExport, commitJSONImport, cancelJSONImport } from '../jsonImport';
import client from '../client';
vi.mock('../client', () => ({
default: {
post: vi.fn(),
},
}));
describe('jsonImport API', () => {
const mockedPost = vi.mocked(client.post);
beforeEach(() => {
vi.clearAllMocks();
});
it('cancelJSONImport posts cancel endpoint with required session_uuid body', async () => {
const sessionUUID = 'json-session-123';
mockedPost.mockResolvedValue({});
await cancelJSONImport(sessionUUID);
expect(client.post).toHaveBeenCalledWith('/import/json/cancel', {
session_uuid: sessionUUID,
});
});
it('uploadJSONExport posts upload endpoint with content payload', async () => {
const content = '{"proxy_hosts":[]}';
const mockResponse = {
session: {
id: 'json-session-456',
state: 'reviewing',
source: 'json',
},
preview: {
hosts: [],
conflicts: [],
errors: [],
},
conflict_details: {},
};
mockedPost.mockResolvedValue({ data: mockResponse });
const result = await uploadJSONExport(content);
expect(client.post).toHaveBeenCalledWith('/import/json/upload', { content });
expect(result).toEqual(mockResponse);
});
it('commitJSONImport posts commit endpoint with session_uuid, resolutions, and names body', async () => {
const sessionUUID = 'json-session-789';
const resolutions = { 'json.example.com': 'replace' };
const names = { 'json.example.com': 'JSON Example' };
const mockResponse = {
created: 1,
updated: 1,
skipped: 0,
errors: [],
};
mockedPost.mockResolvedValue({ data: mockResponse });
const result = await commitJSONImport(sessionUUID, resolutions, names);
expect(client.post).toHaveBeenCalledWith('/import/json/commit', {
session_uuid: sessionUUID,
resolutions,
names,
});
expect(result).toEqual(mockResponse);
});
it('forwards uploadJSONExport errors', async () => {
const error = new Error('upload failed');
mockedPost.mockRejectedValue(error);
await expect(uploadJSONExport('{"proxy_hosts":[]}')).rejects.toBe(error);
});
it('forwards commitJSONImport errors', async () => {
const error = new Error('commit failed');
mockedPost.mockRejectedValue(error);
await expect(commitJSONImport('json-session-123', {}, {})).rejects.toBe(error);
});
it('forwards cancelJSONImport errors', async () => {
const error = new Error('cancel failed');
mockedPost.mockRejectedValue(error);
await expect(cancelJSONImport('json-session-123')).rejects.toBe(error);
});
});

View File

@@ -1,217 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { connectLiveLogs } from '../logs';
// Mock WebSocket
class MockWebSocket {
url: string;
onmessage: ((event: MessageEvent) => void) | null = null;
onerror: ((error: Event) => void) | null = null;
onclose: ((event: CloseEvent) => void) | null = null;
readyState: number = WebSocket.CONNECTING;
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
constructor(url: string) {
this.url = url;
// Simulate connection opening
setTimeout(() => {
this.readyState = WebSocket.OPEN;
}, 0);
}
close() {
this.readyState = WebSocket.CLOSING;
setTimeout(() => {
this.readyState = WebSocket.CLOSED;
const closeEvent = { code: 1000, reason: '', wasClean: true } as CloseEvent;
if (this.onclose) {
this.onclose(closeEvent);
}
}, 0);
}
simulateMessage(data: string) {
if (this.onmessage) {
const event = new MessageEvent('message', { data });
this.onmessage(event);
}
}
simulateError() {
if (this.onerror) {
const event = new Event('error');
this.onerror(event);
}
}
}
describe('logs API - connectLiveLogs', () => {
let mockWebSocket: MockWebSocket;
beforeEach(() => {
// Mock global WebSocket
mockWebSocket = new MockWebSocket('');
(globalThis as typeof globalThis & { WebSocket: typeof WebSocket }).WebSocket = class MockedWebSocket extends MockWebSocket {
constructor(url: string) {
super(url);
// eslint-disable-next-line @typescript-eslint/no-this-alias
mockWebSocket = this;
}
} as unknown as typeof WebSocket;
// Mock window.location
Object.defineProperty(window, 'location', {
value: {
protocol: 'http:',
host: 'localhost:8080',
},
writable: true,
});
});
it('creates WebSocket connection with correct URL', () => {
connectLiveLogs({}, vi.fn());
expect(mockWebSocket.url).toBe('ws://localhost:8080/api/v1/logs/live?');
});
it('uses wss protocol when page is https', () => {
Object.defineProperty(window, 'location', {
value: {
protocol: 'https:',
host: 'example.com',
},
writable: true,
});
connectLiveLogs({}, vi.fn());
expect(mockWebSocket.url).toBe('wss://example.com/api/v1/logs/live?');
});
it('includes filters in query parameters', () => {
connectLiveLogs({ level: 'error', source: 'waf' }, vi.fn());
expect(mockWebSocket.url).toContain('level=error');
expect(mockWebSocket.url).toContain('source=waf');
});
it('calls onMessage callback when message is received', () => {
const mockOnMessage = vi.fn();
connectLiveLogs({}, mockOnMessage);
const logData = {
level: 'info',
timestamp: '2025-12-09T10:30:00Z',
message: 'Test message',
};
mockWebSocket.simulateMessage(JSON.stringify(logData));
expect(mockOnMessage).toHaveBeenCalledWith(logData);
});
it('handles JSON parse errors gracefully', () => {
const mockOnMessage = vi.fn();
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
connectLiveLogs({}, mockOnMessage);
mockWebSocket.simulateMessage('invalid json');
expect(mockOnMessage).not.toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to parse log message:', expect.any(Error));
consoleErrorSpy.mockRestore();
});
// These tests are skipped because the WebSocket mock has timing issues with event handlers
// The functionality is covered by E2E tests
it.skip('calls onError callback when error occurs', async () => {
const mockOnError = vi.fn();
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
connectLiveLogs({}, vi.fn(), mockOnError);
// Wait for handlers to be set up
await new Promise(resolve => setTimeout(resolve, 10));
mockWebSocket.simulateError();
expect(mockOnError).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith('WebSocket error:', expect.any(Event));
consoleErrorSpy.mockRestore();
});
it.skip('calls onClose callback when connection closes', async () => {
const mockOnClose = vi.fn();
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
connectLiveLogs({}, vi.fn(), undefined, mockOnClose);
// Wait for handlers to be set up
await new Promise(resolve => setTimeout(resolve, 10));
mockWebSocket.close();
// Wait for the close event to be processed
await new Promise(resolve => setTimeout(resolve, 20));
expect(mockOnClose).toHaveBeenCalled();
consoleLogSpy.mockRestore();
});
it('returns a close function that closes the WebSocket', async () => {
const closeConnection = connectLiveLogs({}, vi.fn());
// Wait for connection to open
await new Promise(resolve => setTimeout(resolve, 10));
expect(mockWebSocket.readyState).toBe(WebSocket.OPEN);
closeConnection();
expect(mockWebSocket.readyState).toBeGreaterThanOrEqual(WebSocket.CLOSING);
});
it('does not throw when closing already closed connection', () => {
const closeConnection = connectLiveLogs({}, vi.fn());
mockWebSocket.readyState = WebSocket.CLOSED;
expect(() => closeConnection()).not.toThrow();
});
it('handles missing optional callbacks', () => {
// Should not throw with only required onMessage callback
expect(() => connectLiveLogs({}, vi.fn())).not.toThrow();
const mockOnMessage = vi.fn();
connectLiveLogs({}, mockOnMessage);
// Simulate various events
mockWebSocket.simulateMessage(JSON.stringify({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'test' }));
mockWebSocket.simulateError();
expect(mockOnMessage).toHaveBeenCalled();
});
it('processes multiple messages in sequence', () => {
const mockOnMessage = vi.fn();
connectLiveLogs({}, mockOnMessage);
const log1 = { level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Message 1' };
const log2 = { level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Message 2' };
mockWebSocket.simulateMessage(JSON.stringify(log1));
mockWebSocket.simulateMessage(JSON.stringify(log2));
expect(mockOnMessage).toHaveBeenCalledTimes(2);
expect(mockOnMessage).toHaveBeenNthCalledWith(1, log1);
expect(mockOnMessage).toHaveBeenNthCalledWith(2, log2);
});
});

View File

@@ -1,44 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import client from '../client'
import { downloadLog, getLogContent, getLogs } from '../logs'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
},
}))
describe('logs api http helpers', () => {
beforeEach(() => {
vi.clearAllMocks()
Object.defineProperty(window, 'location', {
value: { href: 'http://localhost' },
writable: true,
})
})
it('fetches log list and content with filters', async () => {
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ name: 'access.log', size: 10, mod_time: 'now' }] })
const logs = await getLogs()
expect(logs[0].name).toBe('access.log')
expect(client.get).toHaveBeenCalledWith('/logs')
vi.mocked(client.get).mockResolvedValueOnce({ data: { filename: 'access.log', logs: [], total: 0, limit: 100, offset: 0 } })
const resp = await getLogContent('access.log', {
search: 'bot',
host: 'example.com',
status: '500',
level: 'error',
limit: 50,
offset: 5,
sort: 'asc',
})
expect(resp.filename).toBe('access.log')
expect(client.get).toHaveBeenCalledWith('/logs/access.log?search=bot&host=example.com&status=500&level=error&limit=50&offset=5&sort=asc')
})
it('downloads log via window location', () => {
downloadLog('access.log')
expect(window.location.href).toBe('/api/v1/logs/access.log/download')
})
})

View File

@@ -1,230 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
getChallenge,
createChallenge,
verifyChallenge,
pollChallenge,
deleteChallenge,
} from '../manualChallenge'
import client from '../client'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}))
describe('manualChallenge API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getChallenge', () => {
it('fetches challenge by provider and challenge ID', async () => {
const mockChallenge = {
id: 'challenge-uuid',
status: 'pending',
fqdn: '_acme-challenge.example.com',
value: 'test-value',
ttl: 300,
created_at: '2026-01-11T00:00:00Z',
expires_at: '2026-01-11T00:10:00Z',
dns_propagated: false,
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockChallenge })
const result = await getChallenge(1, 'challenge-uuid')
expect(client.get).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid'
)
expect(result).toEqual(mockChallenge)
})
it('throws error when challenge not found', async () => {
vi.mocked(client.get).mockRejectedValueOnce({
response: { status: 404, data: { error: 'Challenge not found' } },
})
await expect(getChallenge(1, 'invalid-uuid')).rejects.toMatchObject({
response: { status: 404 },
})
})
})
describe('createChallenge', () => {
it('creates a new challenge for the provider', async () => {
const mockChallenge = {
id: 'new-challenge-uuid',
status: 'created',
fqdn: '_acme-challenge.example.com',
value: 'generated-value',
ttl: 300,
created_at: '2026-01-11T00:00:00Z',
expires_at: '2026-01-11T00:10:00Z',
dns_propagated: false,
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockChallenge })
const result = await createChallenge(1, { domain: 'example.com' })
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/manual-challenge', {
domain: 'example.com',
})
expect(result).toEqual(mockChallenge)
})
it('throws error when provider not found', async () => {
vi.mocked(client.post).mockRejectedValueOnce({
response: { status: 404, data: { error: 'Provider not found' } },
})
await expect(createChallenge(999, { domain: 'example.com' })).rejects.toMatchObject({
response: { status: 404 },
})
})
it('throws error when challenge already in progress', async () => {
vi.mocked(client.post).mockRejectedValueOnce({
response: { status: 409, data: { code: 'CHALLENGE_IN_PROGRESS' } },
})
await expect(createChallenge(1, { domain: 'example.com' })).rejects.toMatchObject({
response: { status: 409 },
})
})
})
describe('verifyChallenge', () => {
it('triggers verification for a challenge', async () => {
const mockResult = {
success: true,
dns_found: true,
message: 'TXT record verified successfully',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResult })
const result = await verifyChallenge(1, 'challenge-uuid')
expect(client.post).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid/verify'
)
expect(result).toEqual(mockResult)
})
it('returns dns_found false when record not propagated', async () => {
const mockResult = {
success: false,
dns_found: false,
message: 'DNS record not found',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResult })
const result = await verifyChallenge(1, 'challenge-uuid')
expect(result.success).toBe(false)
expect(result.dns_found).toBe(false)
})
it('throws error when challenge expired', async () => {
vi.mocked(client.post).mockRejectedValueOnce({
response: { status: 410, data: { code: 'CHALLENGE_EXPIRED' } },
})
await expect(verifyChallenge(1, 'challenge-uuid')).rejects.toMatchObject({
response: { status: 410 },
})
})
})
describe('pollChallenge', () => {
it('returns current challenge status', async () => {
const mockPoll = {
status: 'pending',
dns_propagated: false,
time_remaining_seconds: 480,
last_check_at: '2026-01-11T00:02:00Z',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
const result = await pollChallenge(1, 'challenge-uuid')
expect(client.get).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid/poll'
)
expect(result).toEqual(mockPoll)
})
it('returns verified status when DNS propagated', async () => {
const mockPoll = {
status: 'verified',
dns_propagated: true,
time_remaining_seconds: 0,
last_check_at: '2026-01-11T00:05:00Z',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
const result = await pollChallenge(1, 'challenge-uuid')
expect(result.status).toBe('verified')
expect(result.dns_propagated).toBe(true)
})
it('includes error message when challenge failed', async () => {
const mockPoll = {
status: 'failed',
dns_propagated: false,
time_remaining_seconds: 0,
last_check_at: '2026-01-11T00:05:00Z',
error_message: 'ACME validation failed',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
const result = await pollChallenge(1, 'challenge-uuid')
expect(result.status).toBe('failed')
expect(result.error_message).toBe('ACME validation failed')
})
})
describe('deleteChallenge', () => {
it('deletes/cancels a challenge', async () => {
vi.mocked(client.delete).mockResolvedValueOnce({ data: undefined })
await deleteChallenge(1, 'challenge-uuid')
expect(client.delete).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid'
)
})
it('throws error when challenge not found', async () => {
vi.mocked(client.delete).mockRejectedValueOnce({
response: { status: 404, data: { error: 'Challenge not found' } },
})
await expect(deleteChallenge(1, 'invalid-uuid')).rejects.toMatchObject({
response: { status: 404 },
})
})
it('throws error when unauthorized', async () => {
vi.mocked(client.delete).mockRejectedValueOnce({
response: { status: 403, data: { error: 'Unauthorized' } },
})
await expect(deleteChallenge(1, 'challenge-uuid')).rejects.toMatchObject({
response: { status: 403 },
})
})
})
})

View File

@@ -1,111 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import client from '../client'
import {
getProviders,
createProvider,
updateProvider,
deleteProvider,
testProvider,
getTemplates,
previewProvider,
getExternalTemplates,
createExternalTemplate,
updateExternalTemplate,
deleteExternalTemplate,
previewExternalTemplate,
getSecurityNotificationSettings,
updateSecurityNotificationSettings,
} from '../notifications'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}))
describe('notifications api', () => {
beforeEach(() => {
vi.resetAllMocks()
})
it('crud for providers uses correct endpoints', async () => {
vi.mocked(client.get).mockResolvedValue({ data: [{ id: '1', name: 'discord', type: 'discord', url: 'http://', enabled: true } as never] })
vi.mocked(client.post).mockResolvedValue({ data: { id: '2' } })
vi.mocked(client.put).mockResolvedValue({ data: { id: '2', name: 'updated' } })
const providers = await getProviders()
expect(providers[0].id).toBe('1')
expect(client.get).toHaveBeenCalledWith('/notifications/providers')
await createProvider({ name: 'x', type: 'discord' })
expect(client.post).toHaveBeenCalledWith('/notifications/providers', { name: 'x', type: 'discord' })
await updateProvider('2', { name: 'updated', type: 'discord' })
expect(client.put).toHaveBeenCalledWith('/notifications/providers/2', { name: 'updated', type: 'discord' })
await deleteProvider('2')
expect(client.delete).toHaveBeenCalledWith('/notifications/providers/2')
await testProvider({ id: '2', name: 'test', type: 'discord' })
expect(client.post).toHaveBeenCalledWith('/notifications/providers/test', { id: '2', name: 'test', type: 'discord' })
await expect(createProvider({ name: 'x', type: 'slack' })).rejects.toThrow('Unsupported notification provider type: slack')
await expect(updateProvider('2', { name: 'updated', type: 'generic' })).rejects.toThrow('Unsupported notification provider type: generic')
await expect(testProvider({ id: '2', name: 'test', type: 'telegram' })).rejects.toThrow('Unsupported notification provider type: telegram')
})
it('templates and previews use merged payloads', async () => {
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 't1', name: 'default' }] })
const templates = await getTemplates()
expect(templates[0].name).toBe('default')
expect(client.get).toHaveBeenCalledWith('/notifications/templates')
vi.mocked(client.post).mockResolvedValueOnce({ data: { preview: 'ok' } })
const preview = await previewProvider({ name: 'provider', type: 'discord' }, { user: 'alice' })
expect(preview).toEqual({ preview: 'ok' })
expect(client.post).toHaveBeenCalledWith('/notifications/providers/preview', { name: 'provider', type: 'discord', data: { user: 'alice' } })
vi.mocked(client.post).mockResolvedValueOnce({ data: { preview: 'webhook-ok' } })
const webhookPreview = await previewProvider({ name: 'provider', type: 'webhook' }, { user: 'alice' })
expect(webhookPreview).toEqual({ preview: 'webhook-ok' })
expect(client.post).toHaveBeenCalledWith('/notifications/providers/preview', { name: 'provider', type: 'webhook', data: { user: 'alice' } })
})
it('external template endpoints shape payloads', async () => {
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 'ext', name: 'External' }] })
const external = await getExternalTemplates()
expect(external[0].id).toBe('ext')
expect(client.get).toHaveBeenCalledWith('/notifications/external-templates')
vi.mocked(client.post).mockResolvedValueOnce({ data: { id: 'ext2' } })
await createExternalTemplate({ name: 'n' })
expect(client.post).toHaveBeenCalledWith('/notifications/external-templates', { name: 'n' })
vi.mocked(client.put).mockResolvedValueOnce({ data: { id: 'ext', name: 'updated' } })
await updateExternalTemplate('ext', { name: 'updated' })
expect(client.put).toHaveBeenCalledWith('/notifications/external-templates/ext', { name: 'updated' })
await deleteExternalTemplate('ext')
expect(client.delete).toHaveBeenCalledWith('/notifications/external-templates/ext')
vi.mocked(client.post).mockResolvedValueOnce({ data: { id: 'ext2' } })
const result = await previewExternalTemplate('ext', 'tpl', { id: 1 })
expect(result).toEqual({ id: 'ext2' })
expect(client.post).toHaveBeenCalledWith('/notifications/external-templates/preview', { template_id: 'ext', template: 'tpl', data: { id: 1 } })
})
it('reads and updates security notification settings', async () => {
vi.mocked(client.get).mockResolvedValueOnce({ data: { enabled: true, min_log_level: 'info', security_waf_enabled: true, security_acl_enabled: false, security_rate_limit_enabled: true } })
const settings = await getSecurityNotificationSettings()
expect(settings.enabled).toBe(true)
expect(client.get).toHaveBeenCalledWith('/notifications/settings/security')
vi.mocked(client.put).mockResolvedValueOnce({ data: { enabled: false } })
const updated = await updateSecurityNotificationSettings({ enabled: false })
expect(updated.enabled).toBe(false)
expect(client.put).toHaveBeenCalledWith('/notifications/settings/security', { enabled: false })
})
})

View File

@@ -1,96 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { uploadNPMExport, commitNPMImport, cancelNPMImport } from '../npmImport';
import client from '../client';
vi.mock('../client', () => ({
default: {
post: vi.fn(),
},
}));
describe('npmImport API', () => {
const mockedPost = vi.mocked(client.post);
beforeEach(() => {
vi.clearAllMocks();
});
it('cancelNPMImport posts cancel endpoint with required session_uuid body', async () => {
const sessionUUID = 'npm-session-123';
mockedPost.mockResolvedValue({});
await cancelNPMImport(sessionUUID);
expect(client.post).toHaveBeenCalledWith('/import/npm/cancel', {
session_uuid: sessionUUID,
});
});
it('uploadNPMExport posts upload endpoint with content payload', async () => {
const content = '{"proxy_hosts":[]}';
const mockResponse = {
session: {
id: 'npm-session-456',
state: 'reviewing',
source: 'npm',
},
preview: {
hosts: [],
conflicts: [],
errors: [],
},
conflict_details: {},
};
mockedPost.mockResolvedValue({ data: mockResponse });
const result = await uploadNPMExport(content);
expect(client.post).toHaveBeenCalledWith('/import/npm/upload', { content });
expect(result).toEqual(mockResponse);
});
it('commitNPMImport posts commit endpoint with session_uuid, resolutions, and names body', async () => {
const sessionUUID = 'npm-session-789';
const resolutions = { 'npm.example.com': 'replace' };
const names = { 'npm.example.com': 'NPM Example' };
const mockResponse = {
created: 1,
updated: 1,
skipped: 0,
errors: [],
};
mockedPost.mockResolvedValue({ data: mockResponse });
const result = await commitNPMImport(sessionUUID, resolutions, names);
expect(client.post).toHaveBeenCalledWith('/import/npm/commit', {
session_uuid: sessionUUID,
resolutions,
names,
});
expect(result).toEqual(mockResponse);
});
it('forwards uploadNPMExport errors', async () => {
const error = new Error('upload failed');
mockedPost.mockRejectedValue(error);
await expect(uploadNPMExport('{"proxy_hosts":[]}')).rejects.toBe(error);
});
it('forwards commitNPMImport errors', async () => {
const error = new Error('commit failed');
mockedPost.mockRejectedValue(error);
await expect(commitNPMImport('npm-session-123', {}, {})).rejects.toBe(error);
});
it('forwards cancelNPMImport errors', async () => {
const error = new Error('cancel failed');
mockedPost.mockRejectedValue(error);
await expect(cancelNPMImport('npm-session-123')).rejects.toBe(error);
});
});

View File

@@ -1,122 +0,0 @@
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);
});
});
});

View File

@@ -1,465 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as presets from '../presets'
import client from '../client'
vi.mock('../client')
describe('presets API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('listCrowdsecPresets', () => {
it('should fetch presets list with cached flags', async () => {
const mockPresets = {
presets: [
{
slug: 'bot-mitigation-essentials',
title: 'Bot Mitigation Essentials',
summary: 'Core HTTP parsers and scenarios',
source: 'hub',
tags: ['bots', 'web'],
requires_hub: true,
available: true,
cached: true,
cache_key: 'hub-bot-abc123',
etag: '"w/12345"',
retrieved_at: '2025-12-15T10:00:00Z',
},
{
slug: 'honeypot-friendly-defaults',
title: 'Honeypot Friendly Defaults',
summary: 'Lightweight defaults for honeypots',
source: 'builtin',
tags: ['low-noise'],
requires_hub: false,
available: true,
cached: false,
},
],
}
vi.mocked(client.get).mockResolvedValue({ data: mockPresets })
const result = await presets.listCrowdsecPresets()
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets')
expect(result).toEqual(mockPresets)
expect(result.presets).toHaveLength(2)
expect(result.presets[0].cached).toBe(true)
expect(result.presets[0].cache_key).toBe('hub-bot-abc123')
expect(result.presets[1].cached).toBe(false)
})
it('should handle empty presets list', async () => {
const mockData = { presets: [] }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await presets.listCrowdsecPresets()
expect(result.presets).toHaveLength(0)
})
it('should handle API errors', async () => {
const error = new Error('Network error')
vi.mocked(client.get).mockRejectedValue(error)
await expect(presets.listCrowdsecPresets()).rejects.toThrow('Network error')
})
it('should handle hub API unavailability', async () => {
const error = {
response: {
status: 503,
data: { error: 'CrowdSec Hub API unavailable' },
},
}
vi.mocked(client.get).mockRejectedValue(error)
await expect(presets.listCrowdsecPresets()).rejects.toEqual(error)
})
})
describe('getCrowdsecPresets', () => {
it('should be an alias for listCrowdsecPresets', async () => {
const mockData = { presets: [] }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await presets.getCrowdsecPresets()
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets')
expect(result).toEqual(mockData)
})
})
describe('pullCrowdsecPreset', () => {
it('should pull preset and return preview with cache_key', async () => {
const mockResponse = {
status: 'success',
slug: 'bot-mitigation-essentials',
preview: '# Bot Mitigation Config\nconfigs:\n collections:\n - crowdsecurity/base-http-scenarios',
cache_key: 'hub-bot-xyz789',
etag: '"abc123"',
retrieved_at: '2025-12-15T10:00:00Z',
source: 'hub',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await presets.pullCrowdsecPreset('bot-mitigation-essentials')
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/pull', {
slug: 'bot-mitigation-essentials',
})
expect(result).toEqual(mockResponse)
expect(result.status).toBe('success')
expect(result.cache_key).toBeDefined()
expect(result.preview).toContain('configs:')
})
it('should handle invalid preset slug', async () => {
const mockResponse = {
status: 'error',
slug: 'non-existent-preset',
preview: '',
cache_key: '',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await presets.pullCrowdsecPreset('non-existent-preset')
expect(result.status).toBe('error')
})
it('should handle hub API timeout during pull', async () => {
const error = {
response: {
status: 504,
data: { error: 'Gateway timeout while fetching from CrowdSec Hub' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(presets.pullCrowdsecPreset('bot-mitigation-essentials')).rejects.toEqual(error)
})
it('should handle ETAG validation scenarios', async () => {
const mockResponse = {
status: 'success',
slug: 'bot-mitigation-essentials',
preview: '# Cached content',
cache_key: 'hub-bot-cached123',
etag: '"not-modified"',
retrieved_at: '2025-12-14T09:00:00Z',
source: 'cache',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await presets.pullCrowdsecPreset('bot-mitigation-essentials')
expect(result.source).toBe('cache')
expect(result.etag).toBe('"not-modified"')
})
it('should handle CrowdSec not running during pull', async () => {
const error = {
response: {
status: 500,
data: { error: 'CrowdSec LAPI not available' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(presets.pullCrowdsecPreset('bot-mitigation-essentials')).rejects.toEqual(error)
})
it('should encode special characters in preset slug', async () => {
const mockResponse = {
status: 'success',
slug: 'custom/preset-with-slash',
preview: '# Custom',
cache_key: 'custom-key',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
await presets.pullCrowdsecPreset('custom/preset-with-slash')
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/pull', {
slug: 'custom/preset-with-slash',
})
})
})
describe('applyCrowdsecPreset', () => {
it('should apply preset with cache_key when available', async () => {
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'hub-bot-xyz789' }
const mockResponse = {
status: 'success',
backup: '/data/charon/data/backups/preset-backup-20251215-100000.tar.gz',
reload_hint: true,
used_cscli: true,
cache_key: 'hub-bot-xyz789',
slug: 'bot-mitigation-essentials',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await presets.applyCrowdsecPreset(payload)
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/apply', payload)
expect(result).toEqual(mockResponse)
expect(result.status).toBe('success')
expect(result.backup).toBeDefined()
expect(result.reload_hint).toBe(true)
})
it('should apply preset without cache_key (fallback mode)', async () => {
const payload = { slug: 'honeypot-friendly-defaults' }
const mockResponse = {
status: 'success',
backup: '/data/charon/data/backups/preset-backup-20251215-100100.tar.gz',
reload_hint: true,
used_cscli: true,
slug: 'honeypot-friendly-defaults',
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await presets.applyCrowdsecPreset(payload)
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/apply', payload)
expect(result.status).toBe('success')
expect(result.used_cscli).toBe(true)
})
it('should handle stale cache_key gracefully', async () => {
const stalePayload = { slug: 'bot-mitigation-essentials', cache_key: 'old_key_123' }
const error = {
response: {
status: 400,
data: { error: 'Cache key mismatch or expired. Please pull the preset again.' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(presets.applyCrowdsecPreset(stalePayload)).rejects.toEqual(error)
})
it('should error when applying preset with CrowdSec stopped', async () => {
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'valid-key' }
const error = {
response: {
status: 500,
data: { error: 'CrowdSec is not running. Start CrowdSec before applying presets.' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error)
})
it('should handle backup creation failure', async () => {
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'valid-key' }
const error = {
response: {
status: 500,
data: { error: 'Failed to create backup before applying preset' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error)
})
it('should handle cscli errors during application', async () => {
const payload = { slug: 'invalid-preset' }
const error = {
response: {
status: 500,
data: { error: 'cscli hub update failed: exit status 1' },
},
}
vi.mocked(client.post).mockRejectedValue(error)
await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error)
})
it('should handle payload with force flag', async () => {
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'key123' }
const mockResponse = {
status: 'success',
backup: '/data/backups/preset-forced.tar.gz',
reload_hint: true,
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await presets.applyCrowdsecPreset(payload)
expect(result.status).toBe('success')
})
})
describe('getCrowdsecPresetCache', () => {
it('should fetch cached preset preview', async () => {
const mockCache = {
preview: '# Cached Bot Mitigation Config\nconfigs:\n collections:\n - crowdsecurity/base-http-scenarios',
cache_key: 'hub-bot-xyz789',
etag: '"abc123"',
}
vi.mocked(client.get).mockResolvedValue({ data: mockCache })
const result = await presets.getCrowdsecPresetCache('bot-mitigation-essentials')
expect(client.get).toHaveBeenCalledWith(
'/admin/crowdsec/presets/cache/bot-mitigation-essentials'
)
expect(result).toEqual(mockCache)
expect(result.preview).toContain('configs:')
expect(result.cache_key).toBe('hub-bot-xyz789')
})
it('should encode special characters in slug', async () => {
const mockCache = {
preview: '# Custom',
cache_key: 'custom-key',
}
vi.mocked(client.get).mockResolvedValue({ data: mockCache })
await presets.getCrowdsecPresetCache('custom/preset with spaces')
expect(client.get).toHaveBeenCalledWith(
'/admin/crowdsec/presets/cache/custom%2Fpreset%20with%20spaces'
)
})
it('should handle cache miss (404)', async () => {
const error = {
response: {
status: 404,
data: { error: 'Preset not found in cache' },
},
}
vi.mocked(client.get).mockRejectedValue(error)
await expect(presets.getCrowdsecPresetCache('non-cached-preset')).rejects.toEqual(error)
})
it('should handle expired cache entries', async () => {
const error = {
response: {
status: 410,
data: { error: 'Cache entry expired' },
},
}
vi.mocked(client.get).mockRejectedValue(error)
await expect(presets.getCrowdsecPresetCache('expired-preset')).rejects.toEqual(error)
})
it('should handle empty preview content', async () => {
const mockCache = {
preview: '',
cache_key: 'empty-key',
}
vi.mocked(client.get).mockResolvedValue({ data: mockCache })
const result = await presets.getCrowdsecPresetCache('empty-preset')
expect(result.preview).toBe('')
expect(result.cache_key).toBe('empty-key')
})
})
describe('default export', () => {
it('should export all functions', () => {
expect(presets.default).toHaveProperty('listCrowdsecPresets')
expect(presets.default).toHaveProperty('getCrowdsecPresets')
expect(presets.default).toHaveProperty('pullCrowdsecPreset')
expect(presets.default).toHaveProperty('applyCrowdsecPreset')
expect(presets.default).toHaveProperty('getCrowdsecPresetCache')
})
})
describe('integration scenarios', () => {
it('should handle full workflow: list → pull → cache → apply', async () => {
// 1. List presets
const mockList = {
presets: [
{
slug: 'bot-mitigation-essentials',
title: 'Bot Mitigation',
summary: 'Core',
source: 'hub',
requires_hub: true,
available: true,
cached: false,
},
],
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockList })
const listResult = await presets.listCrowdsecPresets()
expect(listResult.presets[0].cached).toBe(false)
// 2. Pull preset
const mockPull = {
status: 'success',
slug: 'bot-mitigation-essentials',
preview: '# Config',
cache_key: 'hub-bot-new123',
etag: '"etag1"',
retrieved_at: '2025-12-15T10:00:00Z',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockPull })
const pullResult = await presets.pullCrowdsecPreset('bot-mitigation-essentials')
expect(pullResult.cache_key).toBe('hub-bot-new123')
// 3. Verify cache
const mockCache = {
preview: '# Config',
cache_key: 'hub-bot-new123',
etag: '"etag1"',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockCache })
const cacheResult = await presets.getCrowdsecPresetCache('bot-mitigation-essentials')
expect(cacheResult.cache_key).toBe(pullResult.cache_key)
// 4. Apply preset
const mockApply = {
status: 'success',
backup: '/data/backups/preset-backup.tar.gz',
reload_hint: true,
cache_key: 'hub-bot-new123',
slug: 'bot-mitigation-essentials',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockApply })
const applyResult = await presets.applyCrowdsecPreset({
slug: 'bot-mitigation-essentials',
cache_key: pullResult.cache_key,
})
expect(applyResult.status).toBe('success')
expect(applyResult.backup).toBeDefined()
})
it('should handle network failure mid-workflow', async () => {
// Pull succeeds
const mockPull = {
status: 'success',
slug: 'test-preset',
preview: '# Test',
cache_key: 'test-key',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockPull })
const pullResult = await presets.pullCrowdsecPreset('test-preset')
expect(pullResult.cache_key).toBe('test-key')
// Apply fails due to network
const networkError = new Error('Network error')
vi.mocked(client.post).mockRejectedValueOnce(networkError)
await expect(
presets.applyCrowdsecPreset({ slug: 'test-preset', cache_key: 'test-key' })
).rejects.toThrow('Network error')
})
})
})

View File

@@ -1,95 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { bulkUpdateACL } from '../proxyHosts';
import type { BulkUpdateACLResponse } from '../proxyHosts';
// Mock the client module
const mockPut = vi.fn();
vi.mock('../client', () => ({
default: {
put: (...args: unknown[]) => mockPut(...args),
},
}));
describe('proxyHosts bulk operations', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('bulkUpdateACL', () => {
it('should apply ACL to multiple hosts', async () => {
const mockResponse: BulkUpdateACLResponse = {
updated: 3,
errors: [],
};
mockPut.mockResolvedValue({ data: mockResponse });
const hostUUIDs = ['uuid-1', 'uuid-2', 'uuid-3'];
const accessListID = 42;
const result = await bulkUpdateACL(hostUUIDs, accessListID);
expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', {
host_uuids: hostUUIDs,
access_list_id: accessListID,
});
expect(result).toEqual(mockResponse);
});
it('should remove ACL from hosts when accessListID is null', async () => {
const mockResponse: BulkUpdateACLResponse = {
updated: 2,
errors: [],
};
mockPut.mockResolvedValue({ data: mockResponse });
const hostUUIDs = ['uuid-1', 'uuid-2'];
const result = await bulkUpdateACL(hostUUIDs, null);
expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', {
host_uuids: hostUUIDs,
access_list_id: null,
});
expect(result).toEqual(mockResponse);
});
it('should handle partial failures', async () => {
const mockResponse: BulkUpdateACLResponse = {
updated: 1,
errors: [
{ uuid: 'invalid-uuid', error: 'proxy host not found' },
],
};
mockPut.mockResolvedValue({ data: mockResponse });
const hostUUIDs = ['valid-uuid', 'invalid-uuid'];
const accessListID = 10;
const result = await bulkUpdateACL(hostUUIDs, accessListID);
expect(result.updated).toBe(1);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].uuid).toBe('invalid-uuid');
});
it('should handle empty host list', async () => {
const mockResponse: BulkUpdateACLResponse = {
updated: 0,
errors: [],
};
mockPut.mockResolvedValue({ data: mockResponse });
const result = await bulkUpdateACL([], 5);
expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', {
host_uuids: [],
access_list_id: 5,
});
expect(result.updated).toBe(0);
});
it('should propagate API errors', async () => {
const error = new Error('Network error');
mockPut.mockRejectedValue(error);
await expect(bulkUpdateACL(['uuid-1'], 1)).rejects.toThrow('Network error');
});
});
});

View File

@@ -1,91 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import client from '../client';
import {
getProxyHosts,
getProxyHost,
createProxyHost,
updateProxyHost,
deleteProxyHost,
testProxyHostConnection,
ProxyHost
} from '../proxyHosts';
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('proxyHosts API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockHost: ProxyHost = {
uuid: '123',
name: 'Example Host',
domain_names: 'example.com',
forward_scheme: 'http',
forward_host: 'localhost',
forward_port: 8080,
ssl_forced: true,
http2_support: true,
hsts_enabled: true,
hsts_subdomains: false,
block_exploits: false,
websocket_support: false,
application: 'none',
locations: [],
enabled: true,
created_at: '2023-01-01',
updated_at: '2023-01-01',
};
it('getProxyHosts calls client.get', async () => {
vi.mocked(client.get).mockResolvedValue({ data: [mockHost] });
const result = await getProxyHosts();
expect(client.get).toHaveBeenCalledWith('/proxy-hosts');
expect(result).toEqual([mockHost]);
});
it('getProxyHost calls client.get with uuid', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockHost });
const result = await getProxyHost('123');
expect(client.get).toHaveBeenCalledWith('/proxy-hosts/123');
expect(result).toEqual(mockHost);
});
it('createProxyHost calls client.post', async () => {
vi.mocked(client.post).mockResolvedValue({ data: mockHost });
const newHost = { domain_names: 'example.com' };
const result = await createProxyHost(newHost);
expect(client.post).toHaveBeenCalledWith('/proxy-hosts', newHost);
expect(result).toEqual(mockHost);
});
it('updateProxyHost calls client.put', async () => {
vi.mocked(client.put).mockResolvedValue({ data: mockHost });
const updates = { enabled: false };
const result = await updateProxyHost('123', updates);
expect(client.put).toHaveBeenCalledWith('/proxy-hosts/123', updates);
expect(result).toEqual(mockHost);
});
it('deleteProxyHost calls client.delete', async () => {
vi.mocked(client.delete).mockResolvedValue({ data: {} });
await deleteProxyHost('123');
expect(client.delete).toHaveBeenCalledWith('/proxy-hosts/123');
});
it('testProxyHostConnection calls client.post', async () => {
vi.mocked(client.post).mockResolvedValue({ data: {} });
await testProxyHostConnection('localhost', 8080);
expect(client.post).toHaveBeenCalledWith('/proxy-hosts/test', {
forward_host: 'localhost',
forward_port: 8080,
});
});
});

View File

@@ -1,146 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import {
getRemoteServers,
getRemoteServer,
createRemoteServer,
updateRemoteServer,
deleteRemoteServer,
testRemoteServerConnection,
testCustomRemoteServerConnection,
} from '../remoteServers';
import client from '../client';
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('remoteServers API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockServer = {
uuid: 'server-123',
name: 'Test Server',
provider: 'docker',
host: '192.168.1.100',
port: 2375,
username: 'admin',
enabled: true,
reachable: true,
last_check: '2024-01-01T12:00:00Z',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T12:00:00Z',
};
describe('getRemoteServers', () => {
it('fetches all servers', async () => {
vi.mocked(client.get).mockResolvedValue({ data: [mockServer] });
const result = await getRemoteServers();
expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: {} });
expect(result).toEqual([mockServer]);
});
it('fetches enabled servers only', async () => {
vi.mocked(client.get).mockResolvedValue({ data: [mockServer] });
const result = await getRemoteServers(true);
expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: { enabled: true } });
expect(result).toEqual([mockServer]);
});
});
describe('getRemoteServer', () => {
it('fetches a single server by UUID', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockServer });
const result = await getRemoteServer('server-123');
expect(client.get).toHaveBeenCalledWith('/remote-servers/server-123');
expect(result).toEqual(mockServer);
});
});
describe('createRemoteServer', () => {
it('creates a new server', async () => {
const newServer = {
name: 'New Server',
provider: 'docker',
host: '10.0.0.1',
port: 2375,
};
vi.mocked(client.post).mockResolvedValue({ data: { ...mockServer, ...newServer } });
const result = await createRemoteServer(newServer);
expect(client.post).toHaveBeenCalledWith('/remote-servers', newServer);
expect(result.name).toBe('New Server');
});
});
describe('updateRemoteServer', () => {
it('updates an existing server', async () => {
const updates = { name: 'Updated Server', enabled: false };
vi.mocked(client.put).mockResolvedValue({ data: { ...mockServer, ...updates } });
const result = await updateRemoteServer('server-123', updates);
expect(client.put).toHaveBeenCalledWith('/remote-servers/server-123', updates);
expect(result.name).toBe('Updated Server');
expect(result.enabled).toBe(false);
});
});
describe('deleteRemoteServer', () => {
it('deletes a server', async () => {
vi.mocked(client.delete).mockResolvedValue({});
await deleteRemoteServer('server-123');
expect(client.delete).toHaveBeenCalledWith('/remote-servers/server-123');
});
});
describe('testRemoteServerConnection', () => {
it('tests connection to an existing server', async () => {
vi.mocked(client.post).mockResolvedValue({ data: { address: '192.168.1.100:2375' } });
const result = await testRemoteServerConnection('server-123');
expect(client.post).toHaveBeenCalledWith('/remote-servers/server-123/test');
expect(result.address).toBe('192.168.1.100:2375');
});
});
describe('testCustomRemoteServerConnection', () => {
it('tests connection to a custom host and port', async () => {
vi.mocked(client.post).mockResolvedValue({
data: { address: '10.0.0.1:2375', reachable: true },
});
const result = await testCustomRemoteServerConnection('10.0.0.1', 2375);
expect(client.post).toHaveBeenCalledWith('/remote-servers/test', { host: '10.0.0.1', port: 2375 });
expect(result.reachable).toBe(true);
});
it('handles unreachable server', async () => {
vi.mocked(client.post).mockResolvedValue({
data: { address: '10.0.0.1:2375', reachable: false, error: 'Connection refused' },
});
const result = await testCustomRemoteServerConnection('10.0.0.1', 2375);
expect(result.reachable).toBe(false);
expect(result.error).toBe('Connection refused');
});
});
});

View File

@@ -1,244 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as security from '../security'
import client from '../client'
vi.mock('../client')
describe('security API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getSecurityStatus', () => {
it('should call GET /security/status', async () => {
const mockData: security.SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { mode: 'local', api_url: 'http://localhost:8080', enabled: true },
waf: { mode: 'enabled', enabled: true },
rate_limit: { mode: 'enabled', enabled: true },
acl: { enabled: true }
}
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await security.getSecurityStatus()
expect(client.get).toHaveBeenCalledWith('/security/status')
expect(result).toEqual(mockData)
})
})
describe('getSecurityConfig', () => {
it('should call GET /security/config', async () => {
const mockData = { config: { admin_whitelist: '10.0.0.0/8' } }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await security.getSecurityConfig()
expect(client.get).toHaveBeenCalledWith('/security/config')
expect(result).toEqual(mockData)
})
})
describe('updateSecurityConfig', () => {
it('should call POST /security/config with payload', async () => {
const payload: security.SecurityConfigPayload = {
name: 'test',
enabled: true,
admin_whitelist: '10.0.0.0/8'
}
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.updateSecurityConfig(payload)
expect(client.post).toHaveBeenCalledWith('/security/config', payload)
expect(result).toEqual(mockData)
})
it('should handle all payload fields', async () => {
const payload: security.SecurityConfigPayload = {
name: 'test',
enabled: true,
admin_whitelist: '10.0.0.0/8',
crowdsec_mode: 'local',
crowdsec_api_url: 'http://localhost:8080',
waf_mode: 'enabled',
waf_rules_source: 'coreruleset',
waf_learning: true,
rate_limit_enable: true,
rate_limit_burst: 10,
rate_limit_requests: 100,
rate_limit_window_sec: 60
}
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.updateSecurityConfig(payload)
expect(client.post).toHaveBeenCalledWith('/security/config', payload)
expect(result).toEqual(mockData)
})
})
describe('generateBreakGlassToken', () => {
it('should call POST /security/breakglass/generate', async () => {
const mockData = { token: 'abc123' }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.generateBreakGlassToken()
expect(client.post).toHaveBeenCalledWith('/security/breakglass/generate')
expect(result).toEqual(mockData)
})
})
describe('enableCerberus', () => {
it('should call POST /security/enable with payload', async () => {
const payload = { mode: 'full' }
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.enableCerberus(payload)
expect(client.post).toHaveBeenCalledWith('/security/enable', payload)
expect(result).toEqual(mockData)
})
it('should call POST /security/enable with empty object when no payload', async () => {
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.enableCerberus()
expect(client.post).toHaveBeenCalledWith('/security/enable', {})
expect(result).toEqual(mockData)
})
})
describe('disableCerberus', () => {
it('should call POST /security/disable with payload', async () => {
const payload = { reason: 'maintenance' }
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.disableCerberus(payload)
expect(client.post).toHaveBeenCalledWith('/security/disable', payload)
expect(result).toEqual(mockData)
})
it('should call POST /security/disable with empty object when no payload', async () => {
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.disableCerberus()
expect(client.post).toHaveBeenCalledWith('/security/disable', {})
expect(result).toEqual(mockData)
})
})
describe('getDecisions', () => {
it('should call GET /security/decisions with default limit', async () => {
const mockData = { decisions: [] }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await security.getDecisions()
expect(client.get).toHaveBeenCalledWith('/security/decisions?limit=50')
expect(result).toEqual(mockData)
})
it('should call GET /security/decisions with custom limit', async () => {
const mockData = { decisions: [] }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await security.getDecisions(100)
expect(client.get).toHaveBeenCalledWith('/security/decisions?limit=100')
expect(result).toEqual(mockData)
})
})
describe('createDecision', () => {
it('should call POST /security/decisions with payload', async () => {
const payload = { value: '1.2.3.4', duration: '4h', type: 'ban' }
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.createDecision(payload)
expect(client.post).toHaveBeenCalledWith('/security/decisions', payload)
expect(result).toEqual(mockData)
})
})
describe('getRuleSets', () => {
it('should call GET /security/rulesets', async () => {
const mockData: security.RuleSetsResponse = {
rulesets: [
{
id: 1,
uuid: 'abc-123',
name: 'OWASP CRS',
source_url: 'https://example.com/rules',
mode: 'blocking',
last_updated: '2025-12-04T00:00:00Z',
content: 'rule content'
}
]
}
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await security.getRuleSets()
expect(client.get).toHaveBeenCalledWith('/security/rulesets')
expect(result).toEqual(mockData)
})
})
describe('upsertRuleSet', () => {
it('should call POST /security/rulesets with create payload', async () => {
const payload: security.UpsertRuleSetPayload = {
name: 'Custom Rules',
content: 'rule content',
mode: 'blocking'
}
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.upsertRuleSet(payload)
expect(client.post).toHaveBeenCalledWith('/security/rulesets', payload)
expect(result).toEqual(mockData)
})
it('should call POST /security/rulesets with update payload', async () => {
const payload: security.UpsertRuleSetPayload = {
id: 1,
name: 'Updated Rules',
source_url: 'https://example.com/rules',
mode: 'detection'
}
const mockData = { success: true }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await security.upsertRuleSet(payload)
expect(client.post).toHaveBeenCalledWith('/security/rulesets', payload)
expect(result).toEqual(mockData)
})
})
describe('deleteRuleSet', () => {
it('should call DELETE /security/rulesets/:id', async () => {
const mockData = { success: true }
vi.mocked(client.delete).mockResolvedValue({ data: mockData })
const result = await security.deleteRuleSet(1)
expect(client.delete).toHaveBeenCalledWith('/security/rulesets/1')
expect(result).toEqual(mockData)
})
})
})

View File

@@ -1,133 +0,0 @@
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', () => {
const mockedGet = vi.mocked(client.get);
const mockedPost = vi.mocked(client.post);
const mockedPut = vi.mocked(client.put);
const mockedDelete = vi.mocked(client.delete);
beforeEach(() => {
vi.clearAllMocks();
});
it('listProfiles returns profiles', async () => {
const mockProfiles = [{ id: 1, name: 'Profile 1' }];
mockedGet.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' };
mockedGet.mockResolvedValue({ data: { profile: mockProfile } });
const result = await securityHeadersApi.getProfile(1);
expect(client.get).toHaveBeenCalledWith('/security/headers/profiles/1');
expect(result).toEqual(mockProfile);
});
it('getProfile accepts UUID string identifiers', async () => {
const mockProfile = { id: 2, uuid: 'profile-uuid', name: 'Profile UUID' };
mockedGet.mockResolvedValue({ data: { profile: mockProfile } });
const result = await securityHeadersApi.getProfile('profile-uuid');
expect(client.get).toHaveBeenCalledWith('/security/headers/profiles/profile-uuid');
expect(result).toEqual(mockProfile);
});
it('createProfile creates a profile', async () => {
const newProfile = { name: 'New Profile' };
const mockResponse = { id: 1, ...newProfile };
mockedPost.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 };
mockedPut.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 () => {
mockedDelete.mockResolvedValue({});
await securityHeadersApi.deleteProfile(1);
expect(client.delete).toHaveBeenCalledWith('/security/headers/profiles/1');
});
it('forwards API errors from listProfiles', async () => {
const error = new Error('backend unavailable');
mockedGet.mockRejectedValue(error);
await expect(securityHeadersApi.listProfiles()).rejects.toBe(error);
});
it('getPresets returns presets', async () => {
const mockPresets = [{ name: 'Basic' }];
mockedGet.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 };
mockedPost.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 };
mockedPost.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: [] };
mockedPost.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'" };
mockedPost.mockResolvedValue({ data: mockResponse });
const result = await securityHeadersApi.buildCSP(directives);
expect(client.post).toHaveBeenCalledWith('/security/headers/csp/build', { directives });
expect(result).toEqual(mockResponse);
});
});

View File

@@ -1,181 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as settings from '../settings'
import client from '../client'
vi.mock('../client')
describe('settings API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getSettings', () => {
it('should call GET /settings', async () => {
const mockData: settings.SettingsMap = {
'ui.theme': 'dark',
'security.cerberus.enabled': 'true'
}
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await settings.getSettings()
expect(client.get).toHaveBeenCalledWith('/settings')
expect(result).toEqual(mockData)
})
})
describe('updateSetting', () => {
it('should call POST /settings with key and value only', async () => {
vi.mocked(client.post).mockResolvedValue({ data: {} })
await settings.updateSetting('ui.theme', 'light')
expect(client.post).toHaveBeenCalledWith('/settings', {
key: 'ui.theme',
value: 'light',
category: undefined,
type: undefined
})
})
it('should call POST /settings with all parameters', async () => {
vi.mocked(client.post).mockResolvedValue({ data: {} })
await settings.updateSetting('security.cerberus.enabled', 'true', 'security', 'bool')
expect(client.post).toHaveBeenCalledWith('/settings', {
key: 'security.cerberus.enabled',
value: 'true',
category: 'security',
type: 'bool'
})
})
it('should call POST /settings with category but no type', async () => {
vi.mocked(client.post).mockResolvedValue({ data: {} })
await settings.updateSetting('ui.theme', 'dark', 'ui')
expect(client.post).toHaveBeenCalledWith('/settings', {
key: 'ui.theme',
value: 'dark',
category: 'ui',
type: undefined
})
})
})
describe('validatePublicURL', () => {
it('should call POST /settings/validate-url with URL', async () => {
const mockResponse = { valid: true, normalized: 'https://example.com' }
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await settings.validatePublicURL('https://example.com')
expect(client.post).toHaveBeenCalledWith('/settings/validate-url', { url: 'https://example.com' })
expect(result).toEqual(mockResponse)
})
it('should return valid: true for valid URL', async () => {
vi.mocked(client.post).mockResolvedValue({ data: { valid: true } })
const result = await settings.validatePublicURL('https://valid.com')
expect(result.valid).toBe(true)
})
it('should return valid: false for invalid URL', async () => {
vi.mocked(client.post).mockResolvedValue({ data: { valid: false, error: 'Invalid URL format' } })
const result = await settings.validatePublicURL('not-a-url')
expect(result.valid).toBe(false)
expect(result.error).toBe('Invalid URL format')
})
it('should return normalized URL when provided', async () => {
vi.mocked(client.post).mockResolvedValue({
data: { valid: true, normalized: 'https://example.com/' }
})
const result = await settings.validatePublicURL('https://example.com')
expect(result.normalized).toBe('https://example.com/')
})
it('should handle validation errors', async () => {
vi.mocked(client.post).mockRejectedValue(new Error('Network error'))
await expect(settings.validatePublicURL('https://example.com')).rejects.toThrow('Network error')
})
it('should handle empty URL parameter', async () => {
vi.mocked(client.post).mockResolvedValue({ data: { valid: false } })
const result = await settings.validatePublicURL('')
expect(client.post).toHaveBeenCalledWith('/settings/validate-url', { url: '' })
expect(result.valid).toBe(false)
})
})
describe('testPublicURL', () => {
it('should call POST /settings/test-url with URL', async () => {
const mockResponse = { reachable: true, latency: 42 }
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await settings.testPublicURL('https://example.com')
expect(client.post).toHaveBeenCalledWith('/settings/test-url', { url: 'https://example.com' })
expect(result).toEqual(mockResponse)
})
it('should return reachable: true with latency for successful test', async () => {
vi.mocked(client.post).mockResolvedValue({
data: { reachable: true, latency: 123, message: 'URL is reachable' }
})
const result = await settings.testPublicURL('https://example.com')
expect(result.reachable).toBe(true)
expect(result.latency).toBe(123)
expect(result.message).toBe('URL is reachable')
})
it('should return reachable: false with error for failed test', async () => {
vi.mocked(client.post).mockResolvedValue({
data: { reachable: false, error: 'Connection timeout' }
})
const result = await settings.testPublicURL('https://unreachable.com')
expect(result.reachable).toBe(false)
expect(result.error).toBe('Connection timeout')
})
it('should return message field when provided', async () => {
vi.mocked(client.post).mockResolvedValue({
data: { reachable: true, latency: 50, message: 'Custom success message' }
})
const result = await settings.testPublicURL('https://example.com')
expect(result.message).toBe('Custom success message')
})
it('should handle request errors', async () => {
vi.mocked(client.post).mockRejectedValue(new Error('Request failed'))
await expect(settings.testPublicURL('https://example.com')).rejects.toThrow('Request failed')
})
it('should handle empty URL parameter', async () => {
vi.mocked(client.post).mockResolvedValue({ data: { reachable: false } })
const result = await settings.testPublicURL('')
expect(client.post).toHaveBeenCalledWith('/settings/test-url', { url: '' })
expect(result.reachable).toBe(false)
})
})
})

View File

@@ -1,23 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import client from '../../api/client'
import { getSetupStatus, performSetup } from '../setup'
describe('setup api', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('getSetupStatus returns status', async () => {
const data = { setupRequired: true }
vi.spyOn(client, 'get').mockResolvedValueOnce({ data })
const res = await getSetupStatus()
expect(res).toEqual(data)
})
it('performSetup posts data to setup endpoint', async () => {
const spy = vi.spyOn(client, 'post').mockResolvedValueOnce({ data: {} })
const payload = { name: 'Admin', email: 'admin@example.com', password: 'secret' }
await performSetup(payload)
expect(spy).toHaveBeenCalledWith('/setup', payload)
})
})

View File

@@ -1,62 +0,0 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import client from '../client'
import { checkUpdates, getNotifications, markNotificationRead, markAllNotificationsRead } from '../system'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
},
}))
describe('System API', () => {
afterEach(() => {
vi.clearAllMocks()
})
it('checkUpdates calls /system/updates', async () => {
const mockData = { available: true, latest_version: '1.0.0', changelog_url: 'url' }
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await checkUpdates()
expect(client.get).toHaveBeenCalledWith('/system/updates')
expect(result).toEqual(mockData)
})
it('getNotifications calls /notifications', async () => {
const mockData = [{ id: '1', title: 'Test' }]
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await getNotifications()
expect(client.get).toHaveBeenCalledWith('/notifications', { params: { unread: false } })
expect(result).toEqual(mockData)
})
it('getNotifications calls /notifications with unreadOnly=true', async () => {
const mockData = [{ id: '1', title: 'Test' }]
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await getNotifications(true)
expect(client.get).toHaveBeenCalledWith('/notifications', { params: { unread: true } })
expect(result).toEqual(mockData)
})
it('markNotificationRead calls /notifications/:id/read', async () => {
vi.mocked(client.post).mockResolvedValue({})
await markNotificationRead('123')
expect(client.post).toHaveBeenCalledWith('/notifications/123/read')
})
it('markAllNotificationsRead calls /notifications/read-all', async () => {
vi.mocked(client.post).mockResolvedValue({})
await markAllNotificationsRead()
expect(client.post).toHaveBeenCalledWith('/notifications/read-all')
})
})

View File

@@ -1,135 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as uptime from '../uptime'
import client from '../client'
import type { UptimeMonitor, UptimeHeartbeat } from '../uptime'
vi.mock('../client')
describe('uptime API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getMonitors', () => {
it('should call GET /uptime/monitors', async () => {
const mockData: UptimeMonitor[] = [
{
id: 'mon-1',
name: 'Test Monitor',
type: 'http',
url: 'https://example.com',
interval: 60,
enabled: true,
status: 'up',
latency: 100,
max_retries: 3
}
]
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await uptime.getMonitors()
expect(client.get).toHaveBeenCalledWith('/uptime/monitors')
expect(result).toEqual(mockData)
})
})
describe('getMonitorHistory', () => {
it('should call GET /uptime/monitors/:id/history with default limit', async () => {
const mockData: UptimeHeartbeat[] = [
{
id: 1,
monitor_id: 'mon-1',
status: 'up',
latency: 100,
message: 'OK',
created_at: '2025-12-04T00:00:00Z'
}
]
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await uptime.getMonitorHistory('mon-1')
expect(client.get).toHaveBeenCalledWith('/uptime/monitors/mon-1/history?limit=50')
expect(result).toEqual(mockData)
})
it('should call GET /uptime/monitors/:id/history with custom limit', async () => {
const mockData: UptimeHeartbeat[] = []
vi.mocked(client.get).mockResolvedValue({ data: mockData })
const result = await uptime.getMonitorHistory('mon-1', 100)
expect(client.get).toHaveBeenCalledWith('/uptime/monitors/mon-1/history?limit=100')
expect(result).toEqual(mockData)
})
})
describe('updateMonitor', () => {
it('should call PUT /uptime/monitors/:id', async () => {
const mockMonitor: UptimeMonitor = {
id: 'mon-1',
name: 'Updated Monitor',
type: 'http',
url: 'https://example.com',
interval: 120,
enabled: false,
status: 'down',
latency: 0,
max_retries: 5
}
vi.mocked(client.put).mockResolvedValue({ data: mockMonitor })
const result = await uptime.updateMonitor('mon-1', { enabled: false, interval: 120 })
expect(client.put).toHaveBeenCalledWith('/uptime/monitors/mon-1', { enabled: false, interval: 120 })
expect(result).toEqual(mockMonitor)
})
})
describe('deleteMonitor', () => {
it('should call DELETE /uptime/monitors/:id', async () => {
vi.mocked(client.delete).mockResolvedValue({ data: undefined })
const result = await uptime.deleteMonitor('mon-1')
expect(client.delete).toHaveBeenCalledWith('/uptime/monitors/mon-1')
expect(result).toBeUndefined()
})
})
describe('syncMonitors', () => {
it('should call POST /uptime/sync with empty body when no params', async () => {
const mockData = { synced: 5 }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await uptime.syncMonitors()
expect(client.post).toHaveBeenCalledWith('/uptime/sync', {})
expect(result).toEqual(mockData)
})
it('should call POST /uptime/sync with provided parameters', async () => {
const mockData = { synced: 5 }
const body = { interval: 120, max_retries: 5 }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await uptime.syncMonitors(body)
expect(client.post).toHaveBeenCalledWith('/uptime/sync', body)
expect(result).toEqual(mockData)
})
})
describe('checkMonitor', () => {
it('should call POST /uptime/monitors/:id/check', async () => {
const mockData = { message: 'Check initiated' }
vi.mocked(client.post).mockResolvedValue({ data: mockData })
const result = await uptime.checkMonitor('mon-1')
expect(client.post).toHaveBeenCalledWith('/uptime/monitors/mon-1/check')
expect(result).toEqual(mockData)
})
})
})

View File

@@ -1,69 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import client from '../client'
import { getProfile, regenerateApiKey, updateProfile } from '../users'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
},
}))
describe('user api', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches profile using masked API key fields', async () => {
vi.mocked(client.get).mockResolvedValueOnce({
data: {
id: 1,
email: 'admin@example.com',
name: 'Admin',
role: 'admin',
has_api_key: true,
api_key_masked: '********',
},
})
const profile = await getProfile()
expect(client.get).toHaveBeenCalledWith('/user/profile')
expect(profile.has_api_key).toBe(true)
expect(profile.api_key_masked).toBe('********')
})
it('regenerates API key and returns metadata-only response', async () => {
vi.mocked(client.post).mockResolvedValueOnce({
data: {
message: 'API key regenerated successfully',
has_api_key: true,
api_key_masked: '********',
api_key_updated: '2026-02-25T00:00:00Z',
},
})
const result = await regenerateApiKey()
expect(client.post).toHaveBeenCalledWith('/user/api-key')
expect(result.has_api_key).toBe(true)
expect(result.api_key_masked).toBe('********')
expect(result.api_key_updated).toBe('2026-02-25T00:00:00Z')
})
it('updates profile with optional current password', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ data: { message: 'ok' } })
await updateProfile({
name: 'Updated Name',
email: 'updated@example.com',
current_password: 'current-password',
})
expect(client.post).toHaveBeenCalledWith('/user/profile', {
name: 'Updated Name',
email: 'updated@example.com',
current_password: 'current-password',
})
})
})

View File

@@ -1,189 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import client from '../client'
import {
listUsers,
getUser,
createUser,
inviteUser,
updateUser,
deleteUser,
updateUserPermissions,
validateInvite,
acceptInvite,
} from '../users'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}))
describe('users api', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('lists, reads, creates, updates, and deletes users', async () => {
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 1, email: 'a' }] })
const users = await listUsers()
expect(users[0].id).toBe(1)
expect(client.get).toHaveBeenCalledWith('/users')
vi.mocked(client.get).mockResolvedValueOnce({ data: { id: 2 } })
await getUser(2)
expect(client.get).toHaveBeenCalledWith('/users/2')
vi.mocked(client.post).mockResolvedValueOnce({ data: { id: 3 } })
await createUser({ email: 'e', name: 'n', password: 'p' })
expect(client.post).toHaveBeenCalledWith('/users', { email: 'e', name: 'n', password: 'p' })
vi.mocked(client.put).mockResolvedValueOnce({ data: { message: 'ok' } })
await updateUser(2, { enabled: false })
expect(client.put).toHaveBeenCalledWith('/users/2', { enabled: false })
vi.mocked(client.delete).mockResolvedValueOnce({ data: { message: 'deleted' } })
await deleteUser(2)
expect(client.delete).toHaveBeenCalledWith('/users/2')
})
it('invites users and updates permissions', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ data: { invite_token_masked: '********', invite_url: '[REDACTED]' } })
await inviteUser({ email: 'i', permission_mode: 'allow_all' })
expect(client.post).toHaveBeenCalledWith('/users/invite', { email: 'i', permission_mode: 'allow_all' })
vi.mocked(client.put).mockResolvedValueOnce({ data: { message: 'saved' } })
await updateUserPermissions(1, { permission_mode: 'deny_all', permitted_hosts: [1, 2] })
expect(client.put).toHaveBeenCalledWith('/users/1/permissions', { permission_mode: 'deny_all', permitted_hosts: [1, 2] })
})
it('validates and accepts invites with params', async () => {
vi.mocked(client.get).mockResolvedValueOnce({ data: { valid: true, email: 'a' } })
await validateInvite('token-1')
expect(client.get).toHaveBeenCalledWith('/invite/validate', { params: { token: 'token-1' } })
vi.mocked(client.post).mockResolvedValueOnce({ data: { message: 'accepted', email: 'a' } })
await acceptInvite({ token: 't', name: 'n', password: 'p' })
expect(client.post).toHaveBeenCalledWith('/invite/accept', { token: 't', name: 'n', password: 'p' })
})
describe('previewInviteURL', () => {
it('should call POST /users/preview-invite-url with email', async () => {
const mockResponse = {
preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
base_url: 'https://example.com',
is_configured: true,
email: 'test@example.com',
warning: false,
warning_message: ''
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await import('../users').then(m => m.previewInviteURL('test@example.com'))
expect(client.post).toHaveBeenCalledWith('/users/preview-invite-url', { email: 'test@example.com' })
expect(result).toEqual(mockResponse)
})
it('should return complete PreviewInviteURLResponse structure', async () => {
const mockResponse = {
preview_url: 'https://charon.example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
base_url: 'https://charon.example.com',
is_configured: true,
email: 'user@test.com',
warning: false,
warning_message: ''
}
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
const result = await import('../users').then(m => m.previewInviteURL('user@test.com'))
expect(result.preview_url).toBeDefined()
expect(result.base_url).toBeDefined()
expect(result.is_configured).toBeDefined()
expect(result.email).toBeDefined()
expect(result.warning).toBeDefined()
expect(result.warning_message).toBeDefined()
})
it('should return preview_url with sample token', async () => {
vi.mocked(client.post).mockResolvedValue({
data: {
preview_url: 'http://localhost:8080/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
base_url: 'http://localhost:8080',
is_configured: false,
email: 'test@example.com',
warning: true,
warning_message: 'Public URL not configured'
}
})
const result = await import('../users').then(m => m.previewInviteURL('test@example.com'))
expect(result.preview_url).toContain('SAMPLE_TOKEN_PREVIEW')
})
it('should return is_configured flag', async () => {
vi.mocked(client.post).mockResolvedValue({
data: {
preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
base_url: 'https://example.com',
is_configured: true,
email: 'test@example.com',
warning: false,
warning_message: ''
}
})
const result = await import('../users').then(m => m.previewInviteURL('test@example.com'))
expect(result.is_configured).toBe(true)
})
it('should return warning flag when public URL not configured', async () => {
vi.mocked(client.post).mockResolvedValue({
data: {
preview_url: 'http://localhost:8080/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
base_url: 'http://localhost:8080',
is_configured: false,
email: 'admin@test.com',
warning: true,
warning_message: 'Using default localhost URL'
}
})
const result = await import('../users').then(m => m.previewInviteURL('admin@test.com'))
expect(result.warning).toBe(true)
expect(result.warning_message).toBe('Using default localhost URL')
})
it('should return the provided email in response', async () => {
const testEmail = 'specific@email.com'
vi.mocked(client.post).mockResolvedValue({
data: {
preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
base_url: 'https://example.com',
is_configured: true,
email: testEmail,
warning: false,
warning_message: ''
}
})
const result = await import('../users').then(m => m.previewInviteURL(testEmail))
expect(result.email).toBe(testEmail)
})
it('should handle request errors', async () => {
vi.mocked(client.post).mockRejectedValue(new Error('Network error'))
await expect(
import('../users').then(m => m.previewInviteURL('test@example.com'))
).rejects.toThrow('Network error')
})
})
})

View File

@@ -1,112 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getWebSocketConnections, getWebSocketStats } from '../websocket';
import client from '../client';
vi.mock('../client');
describe('WebSocket API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getWebSocketConnections', () => {
it('should fetch WebSocket connections', async () => {
const mockResponse = {
connections: [
{
id: 'test-conn-1',
type: 'logs',
connected_at: '2024-01-15T10:00:00Z',
last_activity_at: '2024-01-15T10:05:00Z',
remote_addr: '192.168.1.1:12345',
user_agent: 'Mozilla/5.0',
filters: 'level=error',
},
{
id: 'test-conn-2',
type: 'cerberus',
connected_at: '2024-01-15T10:02:00Z',
last_activity_at: '2024-01-15T10:06:00Z',
remote_addr: '192.168.1.2:54321',
user_agent: 'Chrome/90.0',
filters: 'source=waf',
},
],
count: 2,
};
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
const result = await getWebSocketConnections();
expect(client.get).toHaveBeenCalledWith('/websocket/connections');
expect(result).toEqual(mockResponse);
expect(result.count).toBe(2);
expect(result.connections).toHaveLength(2);
});
it('should handle empty connections', async () => {
const mockResponse = {
connections: [],
count: 0,
};
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
const result = await getWebSocketConnections();
expect(result.connections).toHaveLength(0);
expect(result.count).toBe(0);
});
it('should handle API errors', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Network error'));
await expect(getWebSocketConnections()).rejects.toThrow('Network error');
});
});
describe('getWebSocketStats', () => {
it('should fetch WebSocket statistics', async () => {
const mockResponse = {
total_active: 3,
logs_connections: 2,
cerberus_connections: 1,
oldest_connection: '2024-01-15T09:55:00Z',
last_updated: '2024-01-15T10:10:00Z',
};
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
const result = await getWebSocketStats();
expect(client.get).toHaveBeenCalledWith('/websocket/stats');
expect(result).toEqual(mockResponse);
expect(result.total_active).toBe(3);
expect(result.logs_connections).toBe(2);
expect(result.cerberus_connections).toBe(1);
});
it('should handle stats with no connections', async () => {
const mockResponse = {
total_active: 0,
logs_connections: 0,
cerberus_connections: 0,
last_updated: '2024-01-15T10:10:00Z',
};
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
const result = await getWebSocketStats();
expect(result.total_active).toBe(0);
expect(result.oldest_connection).toBeUndefined();
});
it('should handle API errors', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Server error'));
await expect(getWebSocketStats()).rejects.toThrow('Server error');
});
});
});