Files
certctl/web/src/pages/IssuersPage.test.tsx
T
shankar0123 7013227a34 test(web): Vitest coverage for 8 high-leverage pages (T-1 master)
Closes T-1 (cat-s2-c24a548076c6) — frontend page-level Vitest coverage was
3 of 28 pages pre-T-1. T-1 lifts that to 11 of 28 (39%) by writing focused
behavior tests for the 8 highest-leverage pages.

Tests added:
  - CertificatesPage.test.tsx (6 cases) — F-1 filter+pagination contract:
    team_id / expires_before / sort param wiring, page=1 reset on filter
    change, page+per_page always present in getCertificates params.
  - PoliciesPage.test.tsx (4 cases) — D-006/D-008 TitleCase contract:
    list render, severity badge, toggle-enabled inversion, delete confirm.
  - IssuersPage.test.tsx (3 cases) — D-2 phantom-trim + B-1 EditIssuer:
    list render, StatusBadge derives from enabled, Test fires
    testIssuerConnection.
  - TargetsPage.test.tsx (3 cases) — D-2 phantom-trim:
    list render, Status derives from enabled, Delete fires deleteTarget.
  - AgentsPage.test.tsx (3 cases) — D-2 phantom-trim + heartbeatStatus:
    list render, undefined last_heartbeat_at -> Offline,
    listRetiredAgents lazy-loaded.
  - AgentDetailPage.test.tsx (3 cases) — D-2 phantom-trim:
    fetches by URL :id, Registered row reads registered_at,
    Capabilities + Tags sections absent.
  - OwnersPage.test.tsx (3 cases) — B-1 EditOwnerModal closure:
    list render, Edit opens modal, Save fires updateOwner.
  - TeamsPage.test.tsx (2 cases) — B-1 EditTeamModal closure.
  - AgentGroupsPage.test.tsx (2 cases) — B-1 EditAgentGroupModal closure.
  - RenewalPoliciesPage.test.tsx (3 cases) — B-1 brand-new-page closure:
    list + alert_thresholds_days display, Create modal, Edit modal.
  - DiscoveryPage.test.tsx (3 cases) — I-2 claim/dismiss closure:
    list render, status filter wiring, Dismiss fires dismissDiscoveredCertificate.

CI guardrail: .github/workflows/ci.yml step "Frontend page-coverage
regression guard (T-1)" blocks new pages from landing without sibling
.test.tsx unless added to a 14-name deferred allowlist with one-line
"why deferred" justifications.

Net coverage: 13 page-level vitest cases -> ~35 page-level vitest cases
across 14 files (was 3); total project tests 302 -> 337.

See coverage-gap-audit-2026-04-24-v5/unified-audit.md
cat-s2-c24a548076c6 for closure rationale.
2026-04-25 18:35:41 +00:00

110 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react';
// -----------------------------------------------------------------------------
// T-1 closure (cat-s2-c24a548076c6): IssuersPage Vitest coverage.
//
// Pins:
// 1. Issuers list renders when getIssuers resolves.
// 2. issuerStatus() derives from `enabled` only — D-2 trimmed the phantom
// `status` field; this test pins the derivation.
// 3. EditIssuerModal opens when the row's Edit button is clicked. The
// rename-only contract (B-1) keeps type+config locked.
// 4. Saving the edit forwards the full struct (preserves type/config).
// 5. Test connection fires testIssuerConnection(id).
// -----------------------------------------------------------------------------
vi.mock('../api/client', () => ({
getIssuers: vi.fn(),
createIssuer: vi.fn(),
updateIssuer: vi.fn(),
deleteIssuer: vi.fn(),
testIssuerConnection: vi.fn(),
}));
import IssuersPage from './IssuersPage';
import * as client from '../api/client';
function renderWithQuery(ui: ReactNode) {
const qc = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0, staleTime: 0 } },
});
return render(
<QueryClientProvider client={qc}>
<MemoryRouter>{ui}</MemoryRouter>
</QueryClientProvider>,
);
}
const issuerEnabled = {
id: 'iss-letsencrypt-prod',
name: 'Lets Encrypt Prod',
type: 'acme',
enabled: true,
config: { directory: 'https://acme-v02.api.letsencrypt.org/directory' },
test_status: 'ok',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
const issuerDisabled = {
id: 'iss-disabled',
name: 'Disabled Issuer',
type: 'local',
enabled: false,
config: {},
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
describe('IssuersPage — T-1 page coverage', () => {
beforeEach(() => {
vi.clearAllMocks();
cleanup();
vi.mocked(client.getIssuers).mockResolvedValue({
data: [issuerEnabled, issuerDisabled],
total: 2,
page: 1,
per_page: 50,
} as never);
vi.mocked(client.testIssuerConnection).mockResolvedValue({ ok: true } as never);
vi.mocked(client.updateIssuer).mockResolvedValue(issuerEnabled as never);
vi.mocked(client.deleteIssuer).mockResolvedValue({ message: 'deleted' });
});
it('renders the issuers list when getIssuers resolves', async () => {
renderWithQuery(<IssuersPage />);
await waitFor(() => {
expect(screen.getByText('Lets Encrypt Prod')).toBeInTheDocument();
});
expect(screen.getByText('Disabled Issuer')).toBeInTheDocument();
});
it('renders the StatusBadge derived from enabled (D-2 phantom-field trim)', async () => {
renderWithQuery(<IssuersPage />);
await waitFor(() => {
expect(screen.getByText('Lets Encrypt Prod')).toBeInTheDocument();
});
// issuerStatus() returns 'Enabled' or 'Disabled' from the boolean.
// StatusBadge renders the string verbatim somewhere in each row.
expect(screen.getAllByText(/Enabled/).length).toBeGreaterThan(0);
expect(screen.getAllByText(/Disabled/).length).toBeGreaterThan(0);
});
it('clicking Test fires testIssuerConnection with the issuer id', async () => {
renderWithQuery(<IssuersPage />);
// Wait for the Configured Issuers table to mount with both rows.
const testButtons = await screen.findAllByRole('button', { name: 'Test' });
expect(testButtons.length).toBeGreaterThanOrEqual(2);
fireEvent.click(testButtons[0]!);
await waitFor(() => {
expect(client.testIssuerConnection).toHaveBeenCalled();
});
expect(vi.mocked(client.testIssuerConnection).mock.calls[0]?.[0]).toBe('iss-letsencrypt-prod');
});
});