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%
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
import * as api from '../../api/certificates';
|
||||
import type { Certificate, CertificateDetail } from '../../api/certificates';
|
||||
import {
|
||||
useCertificates,
|
||||
useCertificateDetail,
|
||||
useUploadCertificate,
|
||||
useUpdateCertificate,
|
||||
useDeleteCertificate,
|
||||
useExportCertificate,
|
||||
useValidateCertificate,
|
||||
useBulkDeleteCertificates,
|
||||
} from '../useCertificates';
|
||||
|
||||
vi.mock('../../api/certificates');
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const mockCert: Certificate = {
|
||||
uuid: 'abc-123',
|
||||
domains: 'example.com',
|
||||
issuer: "Let's Encrypt",
|
||||
expires_at: '2025-01-01',
|
||||
status: 'valid',
|
||||
provider: 'letsencrypt',
|
||||
has_key: true,
|
||||
in_use: false,
|
||||
};
|
||||
|
||||
const mockDetail: CertificateDetail = {
|
||||
...mockCert,
|
||||
assigned_hosts: [],
|
||||
chain: [],
|
||||
auto_renew: false,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
};
|
||||
|
||||
describe('useCertificates hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useCertificates', () => {
|
||||
it('fetches certificate list', async () => {
|
||||
vi.mocked(api.getCertificates).mockResolvedValue([mockCert]);
|
||||
|
||||
const { result } = renderHook(() => useCertificates(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
expect(result.current.certificates).toEqual([mockCert]);
|
||||
});
|
||||
|
||||
it('returns empty array when no data', async () => {
|
||||
vi.mocked(api.getCertificates).mockResolvedValue([]);
|
||||
|
||||
const { result } = renderHook(() => useCertificates(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
expect(result.current.certificates).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCertificateDetail', () => {
|
||||
it('fetches certificate detail by uuid', async () => {
|
||||
vi.mocked(api.getCertificateDetail).mockResolvedValue(mockDetail);
|
||||
|
||||
const { result } = renderHook(() => useCertificateDetail('abc-123'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
expect(result.current.detail).toEqual(mockDetail);
|
||||
});
|
||||
|
||||
it('does not fetch when uuid is null', () => {
|
||||
const { result } = renderHook(() => useCertificateDetail(null), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(api.getCertificateDetail).not.toHaveBeenCalled();
|
||||
expect(result.current.detail).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUploadCertificate', () => {
|
||||
it('uploads certificate and invalidates cache', async () => {
|
||||
vi.mocked(api.uploadCertificate).mockResolvedValue(mockCert);
|
||||
|
||||
const { result } = renderHook(() => useUploadCertificate(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({
|
||||
name: 'My Cert',
|
||||
certFile: new File(['cert'], 'cert.pem'),
|
||||
keyFile: new File(['key'], 'key.pem'),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(api.uploadCertificate).toHaveBeenCalledWith(
|
||||
'My Cert',
|
||||
expect.any(File),
|
||||
expect.any(File),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateCertificate', () => {
|
||||
it('updates certificate name', async () => {
|
||||
vi.mocked(api.updateCertificate).mockResolvedValue(mockCert);
|
||||
|
||||
const { result } = renderHook(() => useUpdateCertificate(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ uuid: 'abc-123', name: 'Updated' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(api.updateCertificate).toHaveBeenCalledWith('abc-123', 'Updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteCertificate', () => {
|
||||
it('deletes certificate and invalidates cache', async () => {
|
||||
vi.mocked(api.deleteCertificate).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useDeleteCertificate(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate('abc-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(api.deleteCertificate).toHaveBeenCalledWith('abc-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useExportCertificate', () => {
|
||||
it('exports certificate as blob', async () => {
|
||||
const blob = new Blob(['data']);
|
||||
vi.mocked(api.exportCertificate).mockResolvedValue(blob);
|
||||
|
||||
const { result } = renderHook(() => useExportCertificate(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({
|
||||
uuid: 'abc-123',
|
||||
format: 'pem',
|
||||
includeKey: true,
|
||||
password: 'pass',
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(api.exportCertificate).toHaveBeenCalledWith('abc-123', 'pem', true, 'pass', undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useValidateCertificate', () => {
|
||||
it('validates certificate files', async () => {
|
||||
const validation = {
|
||||
valid: true,
|
||||
common_name: 'example.com',
|
||||
domains: ['example.com'],
|
||||
issuer_org: 'LE',
|
||||
expires_at: '2025-01-01',
|
||||
key_match: true,
|
||||
chain_valid: true,
|
||||
chain_depth: 1,
|
||||
warnings: [],
|
||||
errors: [],
|
||||
};
|
||||
vi.mocked(api.validateCertificate).mockResolvedValue(validation);
|
||||
|
||||
const { result } = renderHook(() => useValidateCertificate(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({
|
||||
certFile: new File(['cert'], 'cert.pem'),
|
||||
keyFile: new File(['key'], 'key.pem'),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(api.validateCertificate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useBulkDeleteCertificates', () => {
|
||||
it('deletes multiple certificates', async () => {
|
||||
vi.mocked(api.deleteCertificate).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useBulkDeleteCertificates(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(['uuid-1', 'uuid-2']);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(api.deleteCertificate).toHaveBeenCalledTimes(2);
|
||||
expect(result.current.data).toEqual({ succeeded: 2, failed: 0 });
|
||||
});
|
||||
|
||||
it('reports partial failures', async () => {
|
||||
vi.mocked(api.deleteCertificate)
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockRejectedValueOnce(new Error('fail'));
|
||||
|
||||
const { result } = renderHook(() => useBulkDeleteCertificates(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(['uuid-1', 'uuid-2']);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data).toEqual({ succeeded: 1, failed: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user