Files
Charon/frontend/src/api/__tests__/certificates.test.ts
GitHub Actions e1bc648dfc test: add certificate feature unit tests and null-safety fix
Add comprehensive unit tests for the certificate upload, export,
and detail management feature:

- CertificateExportDialog: 21 tests covering format selection,
  blob download, error handling, and password-protected exports
- CertificateUploadDialog: 23 tests covering file validation,
  format detection, drag-and-drop, and upload flow
- CertificateDetailDialog: 19 tests covering detail display,
  loading state, missing fields, and branch coverage
- CertificateChainViewer: 8 tests covering chain visualization
- CertificateValidationPreview: 16 tests covering validation display
- FileDropZone: 18 tests covering drag-and-drop interactions
- useCertificates hooks: 10 tests covering all React Query hooks
- certificates API: 7 new tests for previously uncovered endpoints

Fix null-safety issue in ProxyHosts where cert.domains could be
undefined, causing a runtime error on split().

Frontend patch coverage: 90.6%, overall lines: 89.09%
2026-04-13 04:02:31 +00:00

136 lines
4.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getCertificates,
getCertificateDetail,
uploadCertificate,
updateCertificate,
deleteCertificate,
exportCertificate,
validateCertificate,
type Certificate,
type CertificateDetail,
} from '../certificates';
import client from '../client';
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('certificates API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockCert: Certificate = {
uuid: 'abc-123',
domains: 'example.com',
issuer: 'Let\'s Encrypt',
expires_at: '2023-01-01',
status: 'valid',
provider: 'letsencrypt',
has_key: true,
in_use: false,
};
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('abc-123');
expect(client.delete).toHaveBeenCalledWith('/certificates/abc-123');
});
it('getCertificateDetail calls client.get with uuid', async () => {
const detail: CertificateDetail = {
...mockCert,
assigned_hosts: [],
chain: [],
auto_renew: false,
created_at: '2023-01-01',
updated_at: '2023-01-01',
};
vi.mocked(client.get).mockResolvedValue({ data: detail });
const result = await getCertificateDetail('abc-123');
expect(client.get).toHaveBeenCalledWith('/certificates/abc-123');
expect(result).toEqual(detail);
});
it('updateCertificate calls client.put with name', async () => {
vi.mocked(client.put).mockResolvedValue({ data: mockCert });
const result = await updateCertificate('abc-123', 'New Name');
expect(client.put).toHaveBeenCalledWith('/certificates/abc-123', { name: 'New Name' });
expect(result).toEqual(mockCert);
});
it('exportCertificate calls client.post with blob response type', async () => {
const blob = new Blob(['data']);
vi.mocked(client.post).mockResolvedValue({ data: blob });
const result = await exportCertificate('abc-123', 'pem', true, 'pass', 'pfx-pass');
expect(client.post).toHaveBeenCalledWith(
'/certificates/abc-123/export',
{ format: 'pem', include_key: true, password: 'pass', pfx_password: 'pfx-pass' },
{ responseType: 'blob' },
);
expect(result).toEqual(blob);
});
it('validateCertificate calls client.post with FormData', async () => {
const validation = { valid: true, common_name: 'example.com', domains: ['example.com'], issuer_org: 'LE', expires_at: '2024-01-01', key_match: true, chain_valid: true, chain_depth: 1, warnings: [], errors: [] };
vi.mocked(client.post).mockResolvedValue({ data: validation });
const certFile = new File(['cert'], 'cert.pem', { type: 'text/plain' });
const keyFile = new File(['key'], 'key.pem', { type: 'text/plain' });
const result = await validateCertificate(certFile, keyFile);
expect(client.post).toHaveBeenCalledWith('/certificates/validate', expect.any(FormData), {
headers: { 'Content-Type': 'multipart/form-data' },
});
expect(result).toEqual(validation);
});
it('uploadCertificate includes chain file when provided', async () => {
vi.mocked(client.post).mockResolvedValue({ data: mockCert });
const certFile = new File(['cert'], 'cert.pem');
const keyFile = new File(['key'], 'key.pem');
const chainFile = new File(['chain'], 'chain.pem');
await uploadCertificate('My Cert', certFile, keyFile, chainFile);
const formData = vi.mocked(client.post).mock.calls[0][1] as FormData;
expect(formData.get('chain_file')).toBeTruthy();
});
it('validateCertificate includes chain file when provided', async () => {
vi.mocked(client.post).mockResolvedValue({ data: {} });
const certFile = new File(['cert'], 'cert.pem');
const chainFile = new File(['chain'], 'chain.pem');
await validateCertificate(certFile, undefined, chainFile);
const formData = vi.mocked(client.post).mock.calls[0][1] as FormData;
expect(formData.get('chain_file')).toBeTruthy();
expect(formData.get('key_file')).toBeNull();
});
});