mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:21:32 +00:00
test + docs: close 12 test gaps (~250 new tests) and expand testing guide to 34 parts
Implements all P0-P2 test gaps from docs/test-gap-prompt.md: - Deployment service tests (20), target service tests (18), scheduler tests (8) - Agent binary tests (48), CSR renewal tests (8), short-lived cert tests (7) - Domain model tests (25), context cancellation tests (9), concurrency tests (7) - Handler negative-path tests (23 across 5 files) - Frontend error handling tests (86) and API client tests (7) Expands testing-guide.md from 28 to 34 parts covering certificate export, S/MIME/EKU, OCSP/DER CRL, body size limits, Apache/HAProxy connectors, and sub-CA mode. Fixes stale profile count (4->5) and updates sign-off table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,751 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
setApiKey,
|
||||
getCertificates,
|
||||
getCertificate,
|
||||
createCertificate,
|
||||
triggerRenewal,
|
||||
revokeCertificate,
|
||||
exportCertificatePEM,
|
||||
downloadCertificatePEM,
|
||||
exportCertificatePKCS12,
|
||||
getAgents,
|
||||
getAgent,
|
||||
registerAgent,
|
||||
getJobs,
|
||||
cancelJob,
|
||||
approveRenewal,
|
||||
rejectRenewal,
|
||||
getNotifications,
|
||||
getAuditEvents,
|
||||
getPolicies,
|
||||
getIssuers,
|
||||
getTargets,
|
||||
getDiscoveredCertificates,
|
||||
getDiscoveredCertificate,
|
||||
claimDiscoveredCertificate,
|
||||
dismissDiscoveredCertificate,
|
||||
getNetworkScanTargets,
|
||||
getNetworkScanTarget,
|
||||
createNetworkScanTarget,
|
||||
triggerNetworkScan,
|
||||
getDashboardSummary,
|
||||
getMetrics,
|
||||
} from './client';
|
||||
|
||||
// Mock global fetch
|
||||
const mockFetch = vi.fn();
|
||||
globalThis.fetch = mockFetch;
|
||||
|
||||
function mockJsonResponse(data: unknown, status = 200) {
|
||||
return Promise.resolve({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: () => Promise.resolve(data),
|
||||
statusText: 'OK',
|
||||
} as Response);
|
||||
}
|
||||
|
||||
function mockBlobResponse(status = 200) {
|
||||
return Promise.resolve({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
blob: () => Promise.resolve(new Blob(['test data'])),
|
||||
statusText: 'OK',
|
||||
} as Response);
|
||||
}
|
||||
|
||||
function mockErrorResponse(status: number, body: { message?: string; error?: string } = {}) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status,
|
||||
json: () => Promise.resolve(body),
|
||||
statusText: 'Error',
|
||||
} as Response);
|
||||
}
|
||||
|
||||
function mockNetworkError() {
|
||||
return Promise.reject(new TypeError('Failed to fetch'));
|
||||
}
|
||||
|
||||
describe('API Client - Error Handling', () => {
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset();
|
||||
setApiKey(null);
|
||||
});
|
||||
|
||||
// ─── Certificate Endpoints (Network Errors) ──────────────
|
||||
|
||||
describe('Certificate endpoints - Network errors', () => {
|
||||
it('getCertificates propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getCertificates()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('getCertificate propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getCertificate('mc-test')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('createCertificate propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(createCertificate({ common_name: 'test.com' })).rejects.toThrow(
|
||||
'Failed to fetch',
|
||||
);
|
||||
});
|
||||
|
||||
it('triggerRenewal propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(triggerRenewal('mc-test')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('revokeCertificate propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(revokeCertificate('mc-test', 'keyCompromise')).rejects.toThrow(
|
||||
'Failed to fetch',
|
||||
);
|
||||
});
|
||||
|
||||
it('exportCertificatePEM propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(exportCertificatePEM('mc-test')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('downloadCertificatePEM propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(downloadCertificatePEM('mc-test')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('exportCertificatePKCS12 propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(exportCertificatePKCS12('mc-test', 'password')).rejects.toThrow(
|
||||
'Failed to fetch',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Certificate Endpoints (HTTP Errors) ─────────────────
|
||||
|
||||
describe('Certificate endpoints - HTTP error responses', () => {
|
||||
it('getCertificates with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getCertificates()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getCertificates with 403 throws Forbidden', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(403, { message: 'Access denied' }));
|
||||
await expect(getCertificates()).rejects.toThrow('Access denied');
|
||||
});
|
||||
|
||||
it('getCertificate with 404 throws not found message', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(404, { message: 'Certificate not found' }));
|
||||
await expect(getCertificate('mc-nonexistent')).rejects.toThrow(
|
||||
'Certificate not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('createCertificate with 400 throws validation error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(400, { message: 'Invalid common name' }),
|
||||
);
|
||||
await expect(createCertificate({ common_name: 'invalid' })).rejects.toThrow(
|
||||
'Invalid common name',
|
||||
);
|
||||
});
|
||||
|
||||
it('triggerRenewal with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Internal server error' }),
|
||||
);
|
||||
await expect(triggerRenewal('mc-test')).rejects.toThrow('Internal server error');
|
||||
});
|
||||
|
||||
it('revokeCertificate with 429 throws rate limit error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(429, { message: 'Rate limit exceeded' }),
|
||||
);
|
||||
await expect(revokeCertificate('mc-test', 'keyCompromise')).rejects.toThrow(
|
||||
'Rate limit exceeded',
|
||||
);
|
||||
});
|
||||
|
||||
it('downloadCertificatePEM with 404 throws Export failed', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(404));
|
||||
await expect(downloadCertificatePEM('mc-nonexistent')).rejects.toThrow(
|
||||
'Export failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('exportCertificatePKCS12 with 403 throws Export failed', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(403));
|
||||
await expect(exportCertificatePKCS12('mc-test', 'password')).rejects.toThrow(
|
||||
'Export failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('getCertificates falls back to statusText when no message', async () => {
|
||||
const response = Promise.resolve({
|
||||
ok: false,
|
||||
status: 502,
|
||||
json: () => Promise.reject(new Error('not json')),
|
||||
statusText: 'Bad Gateway',
|
||||
} as Response);
|
||||
mockFetch.mockReturnValueOnce(response);
|
||||
await expect(getCertificates()).rejects.toThrow('Bad Gateway');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Certificate Endpoints (Malformed Responses) ─────────
|
||||
|
||||
describe('Certificate endpoints - Malformed responses', () => {
|
||||
it('getCertificates with invalid JSON throws parse error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
||||
} as Response),
|
||||
);
|
||||
await expect(getCertificates()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('getCertificate with empty response body', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 204,
|
||||
json: () => Promise.resolve({}),
|
||||
} as Response),
|
||||
);
|
||||
const result = await getCertificate('mc-test');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Agent Endpoints (Network Errors) ─────────────────────
|
||||
|
||||
describe('Agent endpoints - Network errors', () => {
|
||||
it('getAgents propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getAgents()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('getAgent propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getAgent('a-web01')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('registerAgent propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(registerAgent({ name: 'agent1' })).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Agent Endpoints (HTTP Errors) ─────────────────────────
|
||||
|
||||
describe('Agent endpoints - HTTP error responses', () => {
|
||||
it('getAgents with 401 triggers auth-required event', async () => {
|
||||
const listener = vi.fn();
|
||||
window.addEventListener('certctl:auth-required', listener);
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getAgents()).rejects.toThrow('Authentication required');
|
||||
expect(listener).toHaveBeenCalled();
|
||||
window.removeEventListener('certctl:auth-required', listener);
|
||||
});
|
||||
|
||||
it('getAgent with 404 throws not found', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(404, { message: 'Agent not found' }));
|
||||
await expect(getAgent('a-nonexistent')).rejects.toThrow('Agent not found');
|
||||
});
|
||||
|
||||
it('registerAgent with 400 throws validation error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(400, { message: 'Invalid agent name' }),
|
||||
);
|
||||
await expect(registerAgent({ name: '' })).rejects.toThrow('Invalid agent name');
|
||||
});
|
||||
|
||||
it('getAgents with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Database connection failed' }),
|
||||
);
|
||||
await expect(getAgents()).rejects.toThrow('Database connection failed');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Job Endpoints (Network Errors) ──────────────────────
|
||||
|
||||
describe('Job endpoints - Network errors', () => {
|
||||
it('getJobs propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getJobs()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('cancelJob propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(cancelJob('job-123')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('approveRenewal propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(approveRenewal('job-123')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('rejectRenewal propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(rejectRenewal('job-123', 'Not ready')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Job Endpoints (HTTP Errors) ─────────────────────────
|
||||
|
||||
describe('Job endpoints - HTTP error responses', () => {
|
||||
it('getJobs with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getJobs()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('cancelJob with 400 throws invalid state error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(400, { message: 'Cannot cancel completed job' }),
|
||||
);
|
||||
await expect(cancelJob('job-123')).rejects.toThrow('Cannot cancel completed job');
|
||||
});
|
||||
|
||||
it('approveRenewal with 403 throws Forbidden', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(403, { message: 'Permission denied' }));
|
||||
await expect(approveRenewal('job-123')).rejects.toThrow('Permission denied');
|
||||
});
|
||||
|
||||
it('rejectRenewal with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Failed to process rejection' }),
|
||||
);
|
||||
await expect(rejectRenewal('job-123', 'Too risky')).rejects.toThrow(
|
||||
'Failed to process rejection',
|
||||
);
|
||||
});
|
||||
|
||||
it('getJobs with 429 throws rate limit error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(429, { message: 'Too many requests' }),
|
||||
);
|
||||
await expect(getJobs()).rejects.toThrow('Too many requests');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Notification Endpoints (Network Errors) ─────────────
|
||||
|
||||
describe('Notification endpoints - Network errors', () => {
|
||||
it('getNotifications propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getNotifications()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Notification Endpoints (HTTP Errors) ────────────────
|
||||
|
||||
describe('Notification endpoints - HTTP error responses', () => {
|
||||
it('getNotifications with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getNotifications()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getNotifications with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Cache unavailable' }),
|
||||
);
|
||||
await expect(getNotifications()).rejects.toThrow('Cache unavailable');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Audit Endpoints (Network Errors) ────────────────────
|
||||
|
||||
describe('Audit endpoints - Network errors', () => {
|
||||
it('getAuditEvents propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getAuditEvents()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Audit Endpoints (HTTP Errors) ───────────────────────
|
||||
|
||||
describe('Audit endpoints - HTTP error responses', () => {
|
||||
it('getAuditEvents with 403 throws Forbidden', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(403, { message: 'Audit access denied' }));
|
||||
await expect(getAuditEvents()).rejects.toThrow('Audit access denied');
|
||||
});
|
||||
|
||||
it('getAuditEvents with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Audit log unavailable' }),
|
||||
);
|
||||
await expect(getAuditEvents()).rejects.toThrow('Audit log unavailable');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Policy Endpoints (Network Errors) ───────────────────
|
||||
|
||||
describe('Policy endpoints - Network errors', () => {
|
||||
it('getPolicies propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getPolicies()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Policy Endpoints (HTTP Errors) ──────────────────────
|
||||
|
||||
describe('Policy endpoints - HTTP error responses', () => {
|
||||
it('getPolicies with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getPolicies()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getPolicies with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Policy service error' }),
|
||||
);
|
||||
await expect(getPolicies()).rejects.toThrow('Policy service error');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Issuer Endpoints (Network Errors) ───────────────────
|
||||
|
||||
describe('Issuer endpoints - Network errors', () => {
|
||||
it('getIssuers propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getIssuers()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Issuer Endpoints (HTTP Errors) ──────────────────────
|
||||
|
||||
describe('Issuer endpoints - HTTP error responses', () => {
|
||||
it('getIssuers with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getIssuers()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getIssuers with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Issuer registry error' }),
|
||||
);
|
||||
await expect(getIssuers()).rejects.toThrow('Issuer registry error');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Target Endpoints (Network Errors) ───────────────────
|
||||
|
||||
describe('Target endpoints - Network errors', () => {
|
||||
it('getTargets propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getTargets()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Target Endpoints (HTTP Errors) ──────────────────────
|
||||
|
||||
describe('Target endpoints - HTTP error responses', () => {
|
||||
it('getTargets with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getTargets()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getTargets with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Target registry error' }),
|
||||
);
|
||||
await expect(getTargets()).rejects.toThrow('Target registry error');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Discovery Endpoints (Network Errors) ────────────────
|
||||
|
||||
describe('Discovery endpoints - Network errors', () => {
|
||||
it('getDiscoveredCertificates propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getDiscoveredCertificates()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('getDiscoveredCertificate propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getDiscoveredCertificate('disc-123')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('claimDiscoveredCertificate propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(claimDiscoveredCertificate('disc-123', 'mc-test')).rejects.toThrow(
|
||||
'Failed to fetch',
|
||||
);
|
||||
});
|
||||
|
||||
it('dismissDiscoveredCertificate propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(dismissDiscoveredCertificate('disc-123')).rejects.toThrow(
|
||||
'Failed to fetch',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Discovery Endpoints (HTTP Errors) ───────────────────
|
||||
|
||||
describe('Discovery endpoints - HTTP error responses', () => {
|
||||
it('getDiscoveredCertificates with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getDiscoveredCertificates()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getDiscoveredCertificate with 404 throws not found', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(404, { message: 'Discovered certificate not found' }),
|
||||
);
|
||||
await expect(getDiscoveredCertificate('disc-nonexistent')).rejects.toThrow(
|
||||
'Discovered certificate not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('claimDiscoveredCertificate with 400 throws validation error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(400, { message: 'Certificate already claimed' }),
|
||||
);
|
||||
await expect(claimDiscoveredCertificate('disc-123', 'mc-test')).rejects.toThrow(
|
||||
'Certificate already claimed',
|
||||
);
|
||||
});
|
||||
|
||||
it('dismissDiscoveredCertificate with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Discovery service error' }),
|
||||
);
|
||||
await expect(dismissDiscoveredCertificate('disc-123')).rejects.toThrow(
|
||||
'Discovery service error',
|
||||
);
|
||||
});
|
||||
|
||||
it('getDiscoveredCertificates with 429 throws rate limit error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(429, { message: 'Rate limit exceeded' }),
|
||||
);
|
||||
await expect(getDiscoveredCertificates()).rejects.toThrow('Rate limit exceeded');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Network Scan Endpoints (Network Errors) ─────────────
|
||||
|
||||
describe('Network scan endpoints - Network errors', () => {
|
||||
it('getNetworkScanTargets propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getNetworkScanTargets()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('getNetworkScanTarget propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getNetworkScanTarget('scan-123')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('createNetworkScanTarget propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(
|
||||
createNetworkScanTarget({ name: 'test', cidrs: ['10.0.0.0/24'] }),
|
||||
).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('triggerNetworkScan propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(triggerNetworkScan('scan-123')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Network Scan Endpoints (HTTP Errors) ────────────────
|
||||
|
||||
describe('Network scan endpoints - HTTP error responses', () => {
|
||||
it('getNetworkScanTargets with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getNetworkScanTargets()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getNetworkScanTarget with 404 throws not found', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(404, { message: 'Scan target not found' }),
|
||||
);
|
||||
await expect(getNetworkScanTarget('scan-nonexistent')).rejects.toThrow(
|
||||
'Scan target not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('createNetworkScanTarget with 400 throws validation error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(400, { message: 'Invalid CIDR range' }),
|
||||
);
|
||||
await expect(
|
||||
createNetworkScanTarget({ name: 'test', cidrs: ['invalid'] }),
|
||||
).rejects.toThrow('Invalid CIDR range');
|
||||
});
|
||||
|
||||
it('triggerNetworkScan with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Scanner unavailable' }),
|
||||
);
|
||||
await expect(triggerNetworkScan('scan-123')).rejects.toThrow('Scanner unavailable');
|
||||
});
|
||||
|
||||
it('getNetworkScanTargets with 429 throws rate limit error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(429, { message: 'Scan quota exceeded' }),
|
||||
);
|
||||
await expect(getNetworkScanTargets()).rejects.toThrow('Scan quota exceeded');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Stats/Metrics Endpoints (Network Errors) ────────────
|
||||
|
||||
describe('Stats/Metrics endpoints - Network errors', () => {
|
||||
it('getDashboardSummary propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getDashboardSummary()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('getMetrics propagates network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(getMetrics()).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Stats/Metrics Endpoints (HTTP Errors) ───────────────
|
||||
|
||||
describe('Stats/Metrics endpoints - HTTP error responses', () => {
|
||||
it('getDashboardSummary with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getDashboardSummary()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getDashboardSummary with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Stats aggregation failed' }),
|
||||
);
|
||||
await expect(getDashboardSummary()).rejects.toThrow('Stats aggregation failed');
|
||||
});
|
||||
|
||||
it('getMetrics with 401 throws Authentication required', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getMetrics()).rejects.toThrow('Authentication required');
|
||||
});
|
||||
|
||||
it('getMetrics with 500 throws server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Metrics service error' }),
|
||||
);
|
||||
await expect(getMetrics()).rejects.toThrow('Metrics service error');
|
||||
});
|
||||
|
||||
it('getDashboardSummary with 429 throws rate limit error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(429, { message: 'Metrics rate limit exceeded' }),
|
||||
);
|
||||
await expect(getDashboardSummary()).rejects.toThrow('Metrics rate limit exceeded');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Cross-Cutting Error Handling ────────────────────────
|
||||
|
||||
describe('Cross-cutting error scenarios', () => {
|
||||
it('401 on any endpoint triggers auth-required event once', async () => {
|
||||
const listener = vi.fn();
|
||||
window.addEventListener('certctl:auth-required', listener);
|
||||
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getCertificates()).rejects.toThrow('Authentication required');
|
||||
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
await expect(getAgents()).rejects.toThrow('Authentication required');
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
window.removeEventListener('certctl:auth-required', listener);
|
||||
});
|
||||
|
||||
it('prefers message field over error field', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(400, {
|
||||
message: 'Validation failed',
|
||||
error: 'Fallback error',
|
||||
}),
|
||||
);
|
||||
await expect(getCertificates()).rejects.toThrow('Validation failed');
|
||||
});
|
||||
|
||||
it('uses error field when message unavailable', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
mockErrorResponse(500, { error: 'Only error field present' }),
|
||||
);
|
||||
await expect(getCertificates()).rejects.toThrow('Only error field present');
|
||||
});
|
||||
|
||||
it('falls back to statusText when both fields missing', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
status: 418,
|
||||
json: () => Promise.resolve({}),
|
||||
statusText: "I'm a teapot",
|
||||
} as Response),
|
||||
);
|
||||
await expect(getCertificates()).rejects.toThrow("I'm a teapot");
|
||||
});
|
||||
|
||||
it('preserves error context through async chain', async () => {
|
||||
const err = new Error('Original error');
|
||||
mockFetch.mockReturnValueOnce(Promise.reject(err));
|
||||
await expect(getCertificates()).rejects.toBe(err);
|
||||
});
|
||||
|
||||
it('handles multiple sequential errors correctly', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(500, { error: 'Error 1' }));
|
||||
await expect(getCertificates()).rejects.toThrow('Error 1');
|
||||
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(500, { error: 'Error 2' }));
|
||||
await expect(getAgents()).rejects.toThrow('Error 2');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Binary Response Handling (Export) ────────────────────
|
||||
|
||||
describe('Binary response error handling', () => {
|
||||
it('downloadCertificatePEM with network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(downloadCertificatePEM('mc-test')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('downloadCertificatePEM with server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(500));
|
||||
await expect(downloadCertificatePEM('mc-test')).rejects.toThrow('Export failed');
|
||||
});
|
||||
|
||||
it('exportCertificatePKCS12 with network error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockNetworkError());
|
||||
await expect(exportCertificatePKCS12('mc-test', 'pass')).rejects.toThrow(
|
||||
'Failed to fetch',
|
||||
);
|
||||
});
|
||||
|
||||
it('exportCertificatePKCS12 with server error', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(403));
|
||||
await expect(exportCertificatePKCS12('mc-test', 'pass')).rejects.toThrow(
|
||||
'Export failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('downloadCertificatePEM uses Authorization header on error', async () => {
|
||||
setApiKey('test-key');
|
||||
mockFetch.mockReturnValueOnce(mockErrorResponse(401));
|
||||
try {
|
||||
await downloadCertificatePEM('mc-test');
|
||||
} catch {
|
||||
// Expected to fail
|
||||
}
|
||||
const [, init] = mockFetch.mock.calls[0];
|
||||
expect(init.headers['Authorization']).toBe('Bearer test-key');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -878,4 +878,92 @@ describe('API Client', () => {
|
||||
expect(body.password).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Profile (EKU / S/MIME) ─────────────────────────────
|
||||
|
||||
describe('Profile for EKU Display', () => {
|
||||
it('getProfile fetches profile by ID with EKU data', async () => {
|
||||
const profileData = {
|
||||
id: 'prof-smime',
|
||||
name: 'S/MIME Email',
|
||||
allowed_ekus: ['emailProtection'],
|
||||
max_ttl_seconds: 31536000,
|
||||
enabled: true,
|
||||
};
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse(profileData));
|
||||
const result = await getProfile('prof-smime');
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/profiles/prof-smime');
|
||||
expect(result.allowed_ekus).toEqual(['emailProtection']);
|
||||
});
|
||||
|
||||
it('getProfile returns profile with multiple EKUs', async () => {
|
||||
const profileData = {
|
||||
id: 'prof-tls',
|
||||
name: 'TLS Server',
|
||||
allowed_ekus: ['serverAuth', 'clientAuth'],
|
||||
max_ttl_seconds: 7776000,
|
||||
enabled: true,
|
||||
};
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse(profileData));
|
||||
const result = await getProfile('prof-tls');
|
||||
expect(result.allowed_ekus).toHaveLength(2);
|
||||
expect(result.allowed_ekus).toContain('serverAuth');
|
||||
expect(result.allowed_ekus).toContain('clientAuth');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Job Verification Fields ─────────────────────────────
|
||||
|
||||
describe('Job Verification', () => {
|
||||
it('getJobs returns jobs with verification fields', async () => {
|
||||
const jobData = {
|
||||
data: [{
|
||||
id: 'job-1',
|
||||
certificate_id: 'mc-1',
|
||||
type: 'Deployment',
|
||||
status: 'Completed',
|
||||
verification_status: 'success',
|
||||
verified_at: '2026-03-28T12:00:00Z',
|
||||
verification_fingerprint: 'abc123',
|
||||
verification_error: '',
|
||||
attempts: 1,
|
||||
max_attempts: 3,
|
||||
scheduled_at: '2026-03-28T11:00:00Z',
|
||||
completed_at: '2026-03-28T11:05:00Z',
|
||||
created_at: '2026-03-28T11:00:00Z',
|
||||
}],
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 50,
|
||||
};
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse(jobData));
|
||||
const result = await getJobs({ certificate_id: 'mc-1' });
|
||||
expect(result.data[0].verification_status).toBe('success');
|
||||
expect(result.data[0].verified_at).toBe('2026-03-28T12:00:00Z');
|
||||
expect(result.data[0].verification_fingerprint).toBe('abc123');
|
||||
});
|
||||
|
||||
it('getJobs handles jobs without verification data', async () => {
|
||||
const jobData = {
|
||||
data: [{
|
||||
id: 'job-2',
|
||||
certificate_id: 'mc-2',
|
||||
type: 'Issuance',
|
||||
status: 'Completed',
|
||||
attempts: 1,
|
||||
max_attempts: 3,
|
||||
scheduled_at: '2026-03-28T11:00:00Z',
|
||||
completed_at: '2026-03-28T11:05:00Z',
|
||||
created_at: '2026-03-28T11:00:00Z',
|
||||
}],
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 50,
|
||||
};
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse(jobData));
|
||||
const result = await getJobs({});
|
||||
expect(result.data[0].verification_status).toBeUndefined();
|
||||
expect(result.data[0].verified_at).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,10 @@ export interface Job {
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
created_at: string;
|
||||
verification_status?: string;
|
||||
verified_at?: string;
|
||||
verification_fingerprint?: string;
|
||||
verification_error?: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getCertificate, getCertificateVersions, triggerRenewal, triggerDeployment, archiveCertificate, revokeCertificate, updateCertificate, getTargets, getJobs, getPolicies, getProfiles, downloadCertificatePEM, exportCertificatePKCS12 } from '../api/client';
|
||||
import { getCertificate, getCertificateVersions, triggerRenewal, triggerDeployment, archiveCertificate, revokeCertificate, updateCertificate, getTargets, getJobs, getPolicies, getProfiles, getProfile, downloadCertificatePEM, exportCertificatePKCS12 } from '../api/client';
|
||||
import { REVOCATION_REASONS } from '../api/types';
|
||||
import PageHeader from '../components/PageHeader';
|
||||
import StatusBadge from '../components/StatusBadge';
|
||||
@@ -102,6 +102,28 @@ function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certI
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Verification step (M25: post-deployment TLS verification)
|
||||
const getVerifiedStatus = () => {
|
||||
if (!latestDeploy || latestDeploy.status !== 'Completed') return 'pending' as const;
|
||||
if (latestDeploy.verification_status === 'success') return 'completed' as const;
|
||||
if (latestDeploy.verification_status === 'failed') return 'failed' as const;
|
||||
if (latestDeploy.verification_status === 'skipped') return 'completed' as const;
|
||||
if (latestDeploy.verification_status === 'pending') return 'active' as const;
|
||||
return 'pending' as const;
|
||||
};
|
||||
const getVerifiedTime = () => {
|
||||
if (!latestDeploy || latestDeploy.status !== 'Completed') return undefined;
|
||||
if (latestDeploy.verification_status === 'success' && latestDeploy.verified_at) {
|
||||
return `Verified ${formatDateTime(latestDeploy.verified_at)}`;
|
||||
}
|
||||
if (latestDeploy.verification_status === 'failed') {
|
||||
return latestDeploy.verification_error || 'Verification failed';
|
||||
}
|
||||
if (latestDeploy.verification_status === 'skipped') return 'Skipped (best-effort)';
|
||||
if (latestDeploy.verification_status === 'pending') return 'Awaiting verification';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getActiveStatus = () => {
|
||||
if (certStatus === 'Active') return 'completed' as const;
|
||||
if (certStatus === 'Revoked') return 'failed' as const;
|
||||
@@ -116,6 +138,9 @@ function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certI
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Only show verification step if deployment has completed and verification data exists
|
||||
const showVerificationStep = latestDeploy?.status === 'Completed' && latestDeploy?.verification_status;
|
||||
|
||||
return (
|
||||
<div className="bg-surface border border-surface-border rounded p-5 shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-ink-muted mb-4">Lifecycle Timeline</h3>
|
||||
@@ -123,6 +148,9 @@ function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certI
|
||||
<TimelineStep label="Requested" status={getRequestedStatus()} time={getRequestedTime()} />
|
||||
<TimelineStep label="Issued" status={getIssuedStatus()} time={getIssuedTime()} />
|
||||
<TimelineStep label="Deploying" status={getDeployStatus()} time={getDeployTime()} />
|
||||
{showVerificationStep && (
|
||||
<TimelineStep label="Verified" status={getVerifiedStatus()} time={getVerifiedTime()} />
|
||||
)}
|
||||
<TimelineStep label={certStatus === 'Revoked' ? 'Revoked' : certStatus === 'Expired' ? 'Expired' : 'Active'}
|
||||
status={getActiveStatus()} time={getActiveTime()} isLast />
|
||||
</div>
|
||||
@@ -248,6 +276,13 @@ export default function CertificateDetailPage() {
|
||||
enabled: showDeploy,
|
||||
});
|
||||
|
||||
// Fetch profile for EKU display (S/MIME, code signing badges)
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ['profile', cert?.certificate_profile_id],
|
||||
queryFn: () => getProfile(cert!.certificate_profile_id),
|
||||
enabled: !!cert?.certificate_profile_id,
|
||||
});
|
||||
|
||||
const renewMutation = useMutation({
|
||||
mutationFn: () => triggerRenewal(id!),
|
||||
onSuccess: () => {
|
||||
@@ -465,13 +500,57 @@ export default function CertificateDetailPage() {
|
||||
<h3 className="text-sm font-semibold text-ink-muted mb-4">Certificate Details</h3>
|
||||
<InfoRow label="Status" value={<StatusBadge status={cert.status} />} />
|
||||
<InfoRow label="Common Name" value={cert.common_name} />
|
||||
<InfoRow label="SANs" value={cert.sans?.length ? cert.sans.join(', ') : '—'} />
|
||||
<InfoRow label="SANs" value={cert.sans?.length ? (
|
||||
<span className="text-sm">
|
||||
{cert.sans.map((san, i) => {
|
||||
const isEmail = san.includes('@');
|
||||
return (
|
||||
<span key={san}>
|
||||
{i > 0 && ', '}
|
||||
{isEmail ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="text-xs text-purple-600 bg-purple-50 px-1 rounded">email</span>
|
||||
<span>{san}</span>
|
||||
</span>
|
||||
) : san}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
) : '—'} />
|
||||
<InfoRow label="Serial Number" value={cert.serial_number || '—'} />
|
||||
<InfoRow label="Fingerprint" value={
|
||||
cert.fingerprint ? <span className="font-mono text-xs">{cert.fingerprint.slice(0, 24)}...</span> : '—'
|
||||
} />
|
||||
<InfoRow label="Key Algorithm" value={cert.key_algorithm || '—'} />
|
||||
<InfoRow label="Key Size" value={cert.key_size ? `${cert.key_size} bits` : '—'} />
|
||||
{profile?.allowed_ekus && profile.allowed_ekus.length > 0 && (
|
||||
<InfoRow label="Extended Key Usage" value={
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{profile.allowed_ekus.map(eku => {
|
||||
const ekuStyles: Record<string, string> = {
|
||||
serverAuth: 'bg-blue-50 text-blue-700',
|
||||
clientAuth: 'bg-green-50 text-green-700',
|
||||
emailProtection: 'bg-purple-50 text-purple-700',
|
||||
codeSigning: 'bg-amber-50 text-amber-700',
|
||||
timeStamping: 'bg-teal-50 text-teal-700',
|
||||
};
|
||||
const ekuLabels: Record<string, string> = {
|
||||
serverAuth: 'TLS Server',
|
||||
clientAuth: 'TLS Client',
|
||||
emailProtection: 'S/MIME',
|
||||
codeSigning: 'Code Signing',
|
||||
timeStamping: 'Timestamping',
|
||||
};
|
||||
return (
|
||||
<span key={eku} className={`text-xs px-1.5 py-0.5 rounded font-medium ${ekuStyles[eku] || 'bg-gray-50 text-gray-700'}`}>
|
||||
{ekuLabels[eku] || eku}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lifecycle */}
|
||||
|
||||
@@ -11,16 +11,20 @@ import type { Target } from '../api/types';
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
nginx: 'NGINX',
|
||||
f5_bigip: 'F5 BIG-IP',
|
||||
iis: 'IIS',
|
||||
apache: 'Apache',
|
||||
haproxy: 'HAProxy',
|
||||
traefik: 'Traefik',
|
||||
caddy: 'Caddy',
|
||||
f5_bigip: 'F5 BIG-IP',
|
||||
iis: 'IIS',
|
||||
};
|
||||
|
||||
const TARGET_TYPES = [
|
||||
{ value: 'nginx', label: 'NGINX', description: 'Deploy to NGINX web server via file write + config validation + reload' },
|
||||
{ value: 'apache', label: 'Apache httpd', description: 'Separate cert/chain/key files, apachectl configtest, graceful reload' },
|
||||
{ value: 'haproxy', label: 'HAProxy', description: 'Combined PEM file (cert+chain+key), optional validate, reload' },
|
||||
{ value: 'traefik', label: 'Traefik', description: 'File provider deployment — writes cert/key to watched directory, auto-reload' },
|
||||
{ value: 'caddy', label: 'Caddy', description: 'Admin API hot-reload or file-based deployment with configurable mode' },
|
||||
{ value: 'f5_bigip', label: 'F5 BIG-IP', description: 'iControl REST via proxy agent (V3 implementation)' },
|
||||
{ value: 'iis', label: 'IIS', description: 'Windows IIS via agent-local PowerShell or proxy WinRM (V3 implementation)' },
|
||||
];
|
||||
@@ -43,6 +47,18 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ key: 'reload_cmd', label: 'Reload Command', placeholder: 'systemctl reload haproxy' },
|
||||
{ key: 'validate_cmd', label: 'Validate Command (optional)', placeholder: 'haproxy -c -f /etc/haproxy/haproxy.cfg' },
|
||||
],
|
||||
traefik: [
|
||||
{ key: 'cert_dir', label: 'Certificate Directory', placeholder: '/etc/traefik/certs', required: true },
|
||||
{ key: 'cert_file', label: 'Certificate Filename', placeholder: 'cert.pem (default)' },
|
||||
{ key: 'key_file', label: 'Key Filename', placeholder: 'key.pem (default)' },
|
||||
],
|
||||
caddy: [
|
||||
{ key: 'mode', label: 'Deployment Mode', placeholder: 'api (default) or file', required: true },
|
||||
{ key: 'admin_api', label: 'Admin API URL', placeholder: 'http://localhost:2019 (default)' },
|
||||
{ key: 'cert_dir', label: 'Certificate Directory (file mode)', placeholder: '/etc/caddy/certs' },
|
||||
{ key: 'cert_file', label: 'Certificate Filename', placeholder: 'cert.pem (default)' },
|
||||
{ key: 'key_file', label: 'Key Filename', placeholder: 'key.pem (default)' },
|
||||
],
|
||||
f5_bigip: [
|
||||
{ key: 'management_ip', label: 'Management IP', placeholder: '192.168.1.100', required: true },
|
||||
{ key: 'partition', label: 'Partition', placeholder: 'Common' },
|
||||
|
||||
Reference in New Issue
Block a user