Files
Charon/frontend/src/components/dialogs/__tests__/CertificateExportDialog.test.tsx
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

276 lines
8.8 KiB
TypeScript

import { QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { Certificate } from '../../../api/certificates'
import { createTestQueryClient } from '../../../test/createTestQueryClient'
import CertificateExportDialog from '../CertificateExportDialog'
const exportMutateFn = vi.fn()
vi.mock('../../../hooks/useCertificates', () => ({
useExportCertificate: vi.fn(() => ({
mutate: exportMutateFn,
isPending: false,
})),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: { language: 'en', changeLanguage: vi.fn() },
}),
}))
vi.mock('../../../utils/toast', () => ({
toast: { success: vi.fn(), error: vi.fn() },
}))
const baseCert: Certificate = {
uuid: 'cert-1',
name: 'Test Cert',
domains: 'example.com',
issuer: 'Test CA',
expires_at: '2026-06-01T00:00:00Z',
status: 'valid',
provider: 'custom',
has_key: true,
in_use: false,
}
function renderDialog(
certificate: Certificate | null = baseCert,
open = true,
onOpenChange = vi.fn(),
) {
const qc = createTestQueryClient()
return {
onOpenChange,
...render(
<QueryClientProvider client={qc}>
<CertificateExportDialog
certificate={certificate}
open={open}
onOpenChange={onOpenChange}
/>
</QueryClientProvider>,
),
}
}
describe('CertificateExportDialog', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renders dialog when open', () => {
renderDialog()
expect(screen.getByTestId('certificate-export-dialog')).toBeTruthy()
expect(screen.getByText('certificates.exportTitle')).toBeTruthy()
})
it('does not render when closed', () => {
renderDialog(baseCert, false)
expect(screen.queryByTestId('certificate-export-dialog')).toBeFalsy()
})
it('shows format radio options', () => {
renderDialog()
expect(screen.getByText('certificates.exportFormatPem')).toBeTruthy()
expect(screen.getByText('certificates.exportFormatPfx')).toBeTruthy()
expect(screen.getByText('certificates.exportFormatDer')).toBeTruthy()
})
it('shows include private key checkbox', () => {
renderDialog()
expect(screen.getByText('certificates.includePrivateKey')).toBeTruthy()
})
it('shows export button', () => {
renderDialog()
expect(screen.getByTestId('export-certificate-submit')).toBeTruthy()
})
it('shows cancel button', () => {
renderDialog()
expect(screen.getByText('common.cancel')).toBeTruthy()
})
it('calls onOpenChange(false) on cancel', async () => {
const onOpenChange = vi.fn()
renderDialog(baseCert, true, onOpenChange)
await userEvent.click(screen.getByText('common.cancel'))
expect(onOpenChange).toHaveBeenCalledWith(false)
})
it('selects PEM format by default', () => {
renderDialog()
const pemRadio = screen.getByRole('radio', { name: 'certificates.exportFormatPem' })
expect(pemRadio).toHaveAttribute('aria-checked', 'true')
})
it('can select PFX format', async () => {
renderDialog()
await userEvent.click(screen.getByText('certificates.exportFormatPfx'))
const pfxRadio = screen.getByRole('radio', { name: 'certificates.exportFormatPfx' })
expect(pfxRadio).toHaveAttribute('aria-checked', 'true')
})
it('shows PFX password when PFX format selected', async () => {
renderDialog()
await userEvent.click(screen.getByText('certificates.exportFormatPfx'))
expect(screen.getByText('certificates.exportPfxPassword')).toBeTruthy()
})
it('shows private key warning when include key is checked', async () => {
renderDialog()
const checkbox = screen.getByRole('checkbox')
await userEvent.click(checkbox)
expect(screen.getByText('certificates.includePrivateKeyWarning')).toBeTruthy()
})
it('shows password field when include key is checked', async () => {
renderDialog()
const checkbox = screen.getByRole('checkbox')
await userEvent.click(checkbox)
expect(screen.getByText('certificates.exportPassword')).toBeTruthy()
})
it('calls export mutation on submit', async () => {
renderDialog()
await userEvent.click(screen.getByTestId('export-certificate-submit'))
expect(exportMutateFn).toHaveBeenCalledTimes(1)
expect(exportMutateFn.mock.calls[0][0]).toMatchObject({
uuid: 'cert-1',
format: 'pem',
includeKey: false,
})
})
it('sends include key and password when checked', async () => {
renderDialog()
const checkbox = screen.getByRole('checkbox')
await userEvent.click(checkbox)
const pwInput = document.getElementById('export-password') as HTMLInputElement
await userEvent.type(pwInput, 'secret123')
await userEvent.click(screen.getByTestId('export-certificate-submit'))
expect(exportMutateFn.mock.calls[0][0]).toMatchObject({
uuid: 'cert-1',
format: 'pem',
includeKey: true,
password: 'secret123',
})
})
it('hides include key checkbox when cert has no key', () => {
const certNoKey = { ...baseCert, has_key: false }
renderDialog(certNoKey)
expect(screen.queryByRole('checkbox')).toBeFalsy()
})
it('triggers blob download on export success', async () => {
const fakeBlob = new Blob(['cert-data'], { type: 'application/x-pem-file' })
const revokeURL = vi.fn()
const createURL = vi.fn(() => 'blob:http://localhost/fake')
global.URL.createObjectURL = createURL
global.URL.revokeObjectURL = revokeURL
const appendSpy = vi.spyOn(document.body, 'appendChild')
const removeSpy = vi.fn()
exportMutateFn.mockImplementation(
(_params: unknown, opts: { onSuccess: (b: Blob) => void }) => {
const origCreate = document.createElement.bind(document)
vi.spyOn(document, 'createElement').mockImplementationOnce((tag: string) => {
const el = origCreate(tag) as HTMLAnchorElement
el.remove = removeSpy
return el
})
opts.onSuccess(fakeBlob)
},
)
renderDialog()
await userEvent.click(screen.getByTestId('export-certificate-submit'))
expect(createURL).toHaveBeenCalledWith(fakeBlob)
expect(appendSpy).toHaveBeenCalled()
expect(revokeURL).toHaveBeenCalledWith('blob:http://localhost/fake')
expect(removeSpy).toHaveBeenCalled()
appendSpy.mockRestore()
})
it('shows toast error on export failure', async () => {
const { toast: mockToast } = await import('../../../utils/toast')
exportMutateFn.mockImplementation(
(_params: unknown, opts: { onError: (e: Error) => void }) => {
opts.onError(new Error('Export failed'))
},
)
renderDialog()
await userEvent.click(screen.getByTestId('export-certificate-submit'))
expect(mockToast.error).toHaveBeenCalled()
})
it('selects DER format and submits', async () => {
renderDialog()
await userEvent.click(screen.getByText('certificates.exportFormatDer'))
const derRadio = screen.getByRole('radio', { name: 'certificates.exportFormatDer' })
expect(derRadio).toHaveAttribute('aria-checked', 'true')
await userEvent.click(screen.getByTestId('export-certificate-submit'))
expect(exportMutateFn.mock.calls[0][0]).toMatchObject({
format: 'der',
})
})
it('sends pfxPassword when PFX format selected', async () => {
renderDialog()
await userEvent.click(screen.getByText('certificates.exportFormatPfx'))
const pfxInput = document.getElementById('pfx-password') as HTMLInputElement
await userEvent.type(pfxInput, 'pfx-secret')
await userEvent.click(screen.getByTestId('export-certificate-submit'))
expect(exportMutateFn.mock.calls[0][0]).toMatchObject({
format: 'pfx',
pfxPassword: 'pfx-secret',
})
})
it('returns early from submit when certificate is null', async () => {
renderDialog(null)
// Dialog doesn't render without open+cert, so no submit button to click
// Just verify no calls
expect(exportMutateFn).not.toHaveBeenCalled()
})
it('uses certificate name in download filename on success', async () => {
const fakeBlob = new Blob(['data'])
global.URL.createObjectURL = vi.fn(() => 'blob:fake')
global.URL.revokeObjectURL = vi.fn()
let capturedAnchor: HTMLAnchorElement | null = null
exportMutateFn.mockImplementation(
(_params: unknown, opts: { onSuccess: (b: Blob) => void }) => {
const origCreate = document.createElement.bind(document)
vi.spyOn(document, 'createElement').mockImplementationOnce((tag: string) => {
const el = origCreate(tag) as HTMLAnchorElement
el.remove = vi.fn()
capturedAnchor = el
return el
})
opts.onSuccess(fakeBlob)
},
)
renderDialog()
await userEvent.click(screen.getByTestId('export-certificate-submit'))
expect(capturedAnchor!.download).toBe('Test Cert.pem')
})
})