chore: clean git cache
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -1,218 +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('');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).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);
|
||||
});
|
||||
});
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -1,102 +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.clearAllMocks()
|
||||
})
|
||||
|
||||
it('crud for providers uses correct endpoints', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [{ id: '1', name: 'webhook', type: 'webhook', 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' })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/providers', { name: 'x' })
|
||||
|
||||
await updateProvider('2', { name: 'updated' })
|
||||
expect(client.put).toHaveBeenCalledWith('/notifications/providers/2', { name: 'updated' })
|
||||
|
||||
await deleteProvider('2')
|
||||
expect(client.delete).toHaveBeenCalledWith('/notifications/providers/2')
|
||||
|
||||
await testProvider({ id: '2', name: 'test' })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/providers/test', { id: '2', name: 'test' })
|
||||
})
|
||||
|
||||
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' }, { user: 'alice' })
|
||||
expect(preview).toEqual({ preview: 'ok' })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/providers/preview', { name: 'provider', 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: { rendered: true } })
|
||||
const result = await previewExternalTemplate('ext', 'tpl', { id: 1 })
|
||||
expect(result).toEqual({ rendered: true })
|
||||
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', notify_waf_blocks: 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 })
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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: 't' } })
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user