Add tests for useConsoleEnrollment hooks and crowdsecExport utility functions
- Implement comprehensive tests for the useConsoleStatus and useEnrollConsole hooks, covering various scenarios including success, error handling, and edge cases. - Create unit tests for crowdsecExport utility functions, ensuring filename generation, user input sanitization, and download functionality are thoroughly validated.
This commit is contained in:
506
frontend/src/api/__tests__/consoleEnrollment.test.ts
Normal file
506
frontend/src/api/__tests__/consoleEnrollment.test.ts
Normal file
@@ -0,0 +1,506 @@
|
||||
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: any) {
|
||||
// Error message should NOT contain the key
|
||||
expect(e.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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,56 +4,462 @@ import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('crowdsec presets API', () => {
|
||||
describe('presets API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('lists presets via GET', async () => {
|
||||
const mockData = { presets: [{ slug: 'bot', title: 'Bot', summary: 'desc', source: 'hub', requires_hub: true, available: true, cached: false }] }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
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()
|
||||
const result = await presets.listCrowdsecPresets()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets')
|
||||
expect(result).toEqual(mockData)
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
it('pulls a preset via POST', async () => {
|
||||
const mockData = { status: 'pulled', slug: 'bot', preview: 'configs: {}', cache_key: 'cache-1' }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
describe('getCrowdsecPresets', () => {
|
||||
it('should be an alias for listCrowdsecPresets', async () => {
|
||||
const mockData = { presets: [] }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await presets.pullCrowdsecPreset('bot')
|
||||
const result = await presets.getCrowdsecPresets()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/pull', { slug: 'bot' })
|
||||
expect(result).toEqual(mockData)
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
it('applies a preset via POST', async () => {
|
||||
const mockData = { status: 'applied', backup: '/tmp/backup', cache_key: 'cache-1' }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: 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 payload = { slug: 'bot', cache_key: 'cache-1' }
|
||||
const result = await presets.applyCrowdsecPreset(payload)
|
||||
const result = await presets.pullCrowdsecPreset('bot-mitigation-essentials')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/apply', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches cached preview by slug', async () => {
|
||||
const mockData = { preview: 'cached', cache_key: 'cache-1', etag: 'etag-1' }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
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.getCrowdsecPresetCache('bot/collection')
|
||||
const result = await presets.applyCrowdsecPreset(payload)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets/cache/bot%2Fcollection')
|
||||
expect(result).toEqual(mockData)
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
it('exports default bundle', () => {
|
||||
expect(presets.default).toHaveProperty('listCrowdsecPresets')
|
||||
expect(presets.default).toHaveProperty('pullCrowdsecPreset')
|
||||
expect(presets.default).toHaveProperty('applyCrowdsecPreset')
|
||||
expect(presets.default).toHaveProperty('getCrowdsecPresetCache')
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user