feat(09-04): SplitTab UI + splitCertsAction + render tests

- actions.ts: export SplitEntry + SplitResponse interfaces; add splitCertsAction(file) → POST /split
- SplitTab.tsx: Aufteilen button (disabled without file); per-cert download list (bg-secondary rows)
  each row: subject.cn, validity.notAfter, Herunterladen button → downloadBase64
  empty state / error state (text-destructive) matching InspectTab pattern
- cert-manager.test.tsx: 2 new SplitTab tests (success: 2 download buttons; error: text-destructive)
- All 11 cert-manager web tests green; production files type-clean
This commit is contained in:
2026-07-02 07:16:18 +02:00
parent 2c4ada347c
commit 33b1bc3172
3 changed files with 190 additions and 5 deletions
@@ -43,6 +43,7 @@ vi.mock('next-intl', () => ({
import CertManagerPage from './page';
import { InspectTab } from './components/InspectTab';
import { SplitTab } from './components/SplitTab';
import * as actions from './actions';
afterEach(() => {
@@ -187,3 +188,80 @@ describe('InspectTab', () => {
expect(errorEl.className).toContain('text-destructive');
});
});
// ---------------------------------------------------------------------------
// SplitTab — unit tests
// ---------------------------------------------------------------------------
const MOCK_SPLIT_RESPONSE: actions.SplitResponse = {
count: 2,
certs: [
{
index: 0,
filename: 'cert-1.pem',
content: btoa('-----BEGIN CERTIFICATE-----\nXXX\n-----END CERTIFICATE-----'),
subject: { cn: 'cert1.example.com' },
validity: { notAfter: '2027-01-01T00:00:00.000Z' },
},
{
index: 1,
filename: 'cert-2.pem',
content: btoa('-----BEGIN CERTIFICATE-----\nYYY\n-----END CERTIFICATE-----'),
subject: { cn: 'cert2.example.com' },
validity: { notAfter: '2027-06-01T00:00:00.000Z' },
},
],
};
describe('SplitTab', () => {
it('renders two download buttons after clicking Aufteilen with mocked splitCertsAction', async () => {
vi.spyOn(actions, 'splitCertsAction').mockResolvedValue(MOCK_SPLIT_RESPONSE);
const testFile = new File(['cert data'], 'fullchain.pem', {
type: 'application/x-pem-file',
});
render(<SplitTab file={testFile} pemText="" password="" />);
// Initial empty state
expect(screen.getByText('Keine Datei geladen.')).toBeInTheDocument();
// Click the Aufteilen button (only one in this isolated render)
fireEvent.click(screen.getByText('Aufteilen'));
// Wait for the async action to resolve and per-cert list to appear
await waitFor(() => {
expect(screen.getAllByText('Herunterladen').length).toBe(2);
});
// Empty state is gone
expect(screen.queryByText('Keine Datei geladen.')).not.toBeInTheDocument();
// Both cert CN values are rendered
expect(screen.getByText('cert1.example.com')).toBeInTheDocument();
expect(screen.getByText('cert2.example.com')).toBeInTheDocument();
});
it('shows text-destructive error when splitCertsAction rejects', async () => {
vi.spyOn(actions, 'splitCertsAction').mockRejectedValue(
new Error('invalid format or corrupted file'),
);
const testFile = new File(['garbage'], 'bad.pem', { type: 'text/plain' });
render(<SplitTab file={testFile} pemText="" password="" />);
fireEvent.click(screen.getByText('Aufteilen'));
await waitFor(() => {
expect(
screen.getByText('Unbekanntes Format. Die Datei konnte nicht als Zertifikat erkannt werden.'),
).toBeInTheDocument();
});
const errorEl = screen.getByText(
'Unbekanntes Format. Die Datei konnte nicht als Zertifikat erkannt werden.',
);
expect(errorEl.className).toContain('text-destructive');
});
});