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:
GitHub Actions
2025-12-15 14:45:56 +00:00
parent 5b2724a2ba
commit 11a03de3b7
9 changed files with 2738 additions and 32 deletions

View 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')
})
})
})

View File

@@ -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')
})
})
})