mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:01:30 +00:00
70ebef5d3a
Audit 2026-05-10 HIGH-8 closure landed a parseWWWAuthenticateCause()
call in api/client.ts (line 144) that reads res.headers.get(...) on the
401 path. The two test files in web/src/api/ both provide a Response
mock with no headers property, so every 401 test threw 'Cannot read
properties of undefined (reading get)' instead of the expected
'Authentication required'.
13 tests fail without this fix: 12 in client.error.test.ts (one per
401-mapped endpoint helper) + 1 in client.test.ts (the auth-required
event-dispatch test).
Fix: add headers: { get: () => null } to both mockErrorResponse helpers.
The null return short-circuits parseWWWAuthenticateCause to the default
'Authentication required' message, so every existing 401 assertion
keeps passing.
744 lines
28 KiB
TypeScript
744 lines
28 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
setApiKey,
|
|
getCertificates,
|
|
getCertificate,
|
|
createCertificate,
|
|
triggerRenewal,
|
|
revokeCertificate,
|
|
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;
|
|
|
|
// This file is the error-path companion to client.test.ts; every test
|
|
// uses mockErrorResponse (defined below) to drive a non-2xx response
|
|
// through the client function under test. The success-path
|
|
// (mockJsonResponse / mockBlobResponse) helpers were drafted alongside
|
|
// this scaffolding but never used — CodeQL alert #3 caught the
|
|
// mockJsonResponse leftover. Both helpers were removed for consistency
|
|
// with the file's error-only scope; success-path coverage lives in
|
|
// client.test.ts.
|
|
|
|
function mockErrorResponse(status: number, body: { message?: string; error?: string } = {}) {
|
|
return Promise.resolve({
|
|
ok: false,
|
|
status,
|
|
json: () => Promise.resolve(body),
|
|
statusText: 'Error',
|
|
// Audit 2026-05-10 HIGH-8 closure landed a WWW-Authenticate-header
|
|
// read in the 401 path (src/api/client.ts L144). The mock needs a
|
|
// headers.get() so the read doesn't throw against an undefined.
|
|
headers: { get: () => null } as unknown as Headers,
|
|
} 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',
|
|
);
|
|
});
|
|
|
|
// B-1 closure (cat-b-9b97ffb35ef7): exportCertificatePEM removed as a
|
|
// dead duplicate of downloadCertificatePEM (zero consumers).
|
|
|
|
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');
|
|
});
|
|
});
|
|
});
|