mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 17:38:53 +00:00
8564e2fcd6
Audit 2026-05-11 Fix 12 closure. The original GUI-batch commit
661b6db claimed 'npx tsc --noEmit PASS' but shipped no Vitest
cases for the new surfaces, leaving the regression-prevention
layer wide open. This closure backfills 35 cases across five
files; the next refactor of KeysPage's assign modal that drops
scope_type, or the AuthProvider demo-banner predicate that
gets flipped to !authRequired, surfaces in CI instead of
silently shipping.
What's added:
* web/src/pages/auth/UsersPage.test.tsx (NEW, 8 cases) — pins
the MED-11 closure's UsersPage flow: active rows render the
Active status pill, deactivated rows render dimmed with the
Deactivated <timestamp> status, Deactivate button fires the
API call after confirm() returns true and is a no-op on
false, Reactivate button works inversely, provider filter
narrows the underlying authListUsers call (undefined vs
provider-id), empty list renders the placeholder, loading
renders 'Loading users…'.
* web/src/pages/auth/AuthSettingsPage.test.tsx (EXTENDED, +4
cases) — the pre-existing 2 cases only exercised identity +
bootstrap status; the runtime-config panel (MED-12 closure)
had no test. New cases cover: per-key row rendering,
alphabetical sort (stable for log-scraping correlation),
empty-value '(empty)' placeholder, 403 rejected query
silently hides the panel (non-admins shouldn't see the
shell).
* web/src/pages/auth/KeysPage.test.tsx (EXTENDED, +8 cases) —
the HIGH-10 GUI half added scope picker + scope_id input +
expires_at datetime-local to the assign modal but the
pre-existing test only asserted (actor, role). New cases
pin the third opts arg shape: global hides scope_id input,
profile/issuer scope reveal scope_id + mark required,
trimmed scope_id round-trips into the body, global omits
scope_id (undefined NOT empty string), empty expires_at
omits the field, filled expires_at gets :00Z appended for
RFC3339 promotion, whitespace-only scope_id fires the
'scope_id is required' typed error WITHOUT calling the
API, actor-demo-anon row hides both assign and revoke
affordances.
* web/src/pages/auth/RoleDetailPage.test.tsx (NEW, 9 cases) —
no test file pre-Fix 12. Pins the MED-8 scope picker for
AddPermissionForm: global hides scope_id, profile reveals +
gates the Add button until scope_id is filled, submit POSTs
{permission, scope_type: profile, scope_id} with whitespace
trimming, global submit omits scope keys entirely, issuer
scope path, Add button stays disabled without a permission
selection. Plus the LOW-11 default-role delete-button hide:
r-admin renders the role-delete-disabled-tooltip + NO
role-delete-button, r-auditor same, custom role renders the
delete button. The DEFAULT_ROLE_IDS set tracking the
migration-seeded role ids is the load-bearing client-side
decision so a future drift between migrations and the GUI
set surfaces here too.
* web/src/components/AuthProvider.test.tsx (NEW, 5 cases) —
the LOW-1 demo banner had no test for its visibility
predicate. Pins all four authType branches (none → visible,
api-key → hidden, oidc → hidden, loading → hidden to avoid
flash) plus the rejected-getAuthInfo branch: the catch
treats failure as an old-server-fallback to demo mode (no
authType mutation, loading flips false), so the banner
SHOWS — that's the actual behavior, and pinning it prevents
a future change from silently hiding the banner when the
/auth/info endpoint is unreachable.
Spec deviations: Phase 6 (Layout.test.tsx users-nav) and
Phase 7 (per-Fix tests for Fixes 03/05/07/09/10) live on those
fixes' own branches — already authored there. Including them
here would have produced merge conflicts.
Verify gate:
* tsc --noEmit — clean
* vitest run touched files — 40/40 pass (8 + 6 + 12 + 9 + 5,
including the 2 + 4 + 4 pre-existing cases in the extended
AuthSettingsPage + KeysPage files)
* full suite (162 tests across 15 files) green — no regression
from the panel-mount-in-existing-page setup or the new
mocked-module entries.
Refs cowork/auth-bundles-fixes-2026-05-11/12-test-vitest-gui-coverage.md.
179 lines
7.1 KiB
TypeScript
179 lines
7.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, waitFor, cleanup } from '@testing-library/react';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import type { ReactNode } from 'react';
|
|
|
|
// =============================================================================
|
|
// Bundle 1 Phase 10 — AuthSettingsPage stub coverage. Pins the
|
|
// identity surface + bootstrap-status surface.
|
|
// =============================================================================
|
|
|
|
vi.mock('../../api/client', () => ({
|
|
authMe: vi.fn(),
|
|
authBootstrapAvailable: vi.fn(),
|
|
// Audit 2026-05-11 Fix 12 — runtime-config panel coverage. The page
|
|
// calls authRuntimeConfig via TanStack Query (retry: false), so a
|
|
// rejected mock makes the panel quietly absent. Tests mock it as
|
|
// needed; the two pre-existing tests rely on the panel being absent
|
|
// (no positive assertion against it) so the rejected default works.
|
|
authRuntimeConfig: vi.fn(),
|
|
}));
|
|
|
|
import AuthSettingsPage from './AuthSettingsPage';
|
|
import * as client from '../../api/client';
|
|
|
|
function renderWithProviders(ui: ReactNode) {
|
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
return render(
|
|
<QueryClientProvider client={qc}>
|
|
<MemoryRouter>{ui}</MemoryRouter>
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
cleanup();
|
|
});
|
|
|
|
describe('AuthSettingsPage', () => {
|
|
it('renders identity + bootstrap status (closed)', async () => {
|
|
vi.mocked(client.authMe).mockResolvedValue({
|
|
actor_id: 'alice',
|
|
actor_type: 'APIKey',
|
|
tenant_id: 't-default',
|
|
admin: true,
|
|
roles: ['r-admin'],
|
|
effective_permissions: [{ permission: 'cert.read', scope_type: 'global' }],
|
|
});
|
|
vi.mocked(client.authBootstrapAvailable).mockResolvedValue({ available: false });
|
|
|
|
renderWithProviders(<AuthSettingsPage />);
|
|
|
|
await waitFor(() => screen.getByTestId('auth-settings-roles'));
|
|
expect(screen.getByTestId('auth-settings-roles').textContent).toBe('r-admin');
|
|
expect(screen.getByTestId('auth-settings-permcount').textContent).toBe('1');
|
|
expect(screen.getByTestId('auth-settings-admin').textContent).toBe('yes');
|
|
await waitFor(() => screen.getByTestId('auth-settings-bootstrap-status'));
|
|
expect(screen.getByTestId('auth-settings-bootstrap-status').textContent).toBe('closed');
|
|
});
|
|
|
|
it('flags an open bootstrap path with the OPEN status', async () => {
|
|
vi.mocked(client.authMe).mockResolvedValue({
|
|
actor_id: '',
|
|
actor_type: '',
|
|
tenant_id: 't-default',
|
|
admin: false,
|
|
roles: [],
|
|
effective_permissions: [],
|
|
});
|
|
vi.mocked(client.authBootstrapAvailable).mockResolvedValue({ available: true });
|
|
|
|
renderWithProviders(<AuthSettingsPage />);
|
|
await waitFor(() => screen.getByTestId('auth-settings-bootstrap-status'));
|
|
expect(screen.getByTestId('auth-settings-bootstrap-status').textContent).toMatch(/OPEN/);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Audit 2026-05-11 Fix 12 — AuthSettingsPage runtime-config panel coverage.
|
|
//
|
|
// The MED-12 closure added the auth-runtime-config panel
|
|
// (`data-testid="auth-settings-runtime-config"`) but the pre-existing tests
|
|
// don't exercise it. This block pins:
|
|
// - Happy path renders one <tr> per key in the flat map.
|
|
// - Sort is alphabetical by key — operators rely on stable ordering when
|
|
// correlating CERTCTL_* config across logs and the GUI.
|
|
// - Empty string values render the "(empty)" placeholder, NOT a blank cell
|
|
// (otherwise the row visually disappears).
|
|
// - 403 / rejected query hides the panel silently — non-admins shouldn't
|
|
// see a half-rendered shell.
|
|
// =============================================================================
|
|
|
|
function setupAuthMeAdmin() {
|
|
vi.mocked(client.authMe).mockResolvedValue({
|
|
actor_id: 'admin',
|
|
actor_type: 'APIKey',
|
|
tenant_id: 't-default',
|
|
admin: true,
|
|
roles: ['r-admin'],
|
|
effective_permissions: [{ permission: 'auth.role.assign', scope_type: 'global' }],
|
|
});
|
|
vi.mocked(client.authBootstrapAvailable).mockResolvedValue({ available: false });
|
|
}
|
|
|
|
describe('AuthSettingsPage — runtime config panel (MED-12)', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
cleanup();
|
|
});
|
|
|
|
it('renders one table row per runtime-config key', async () => {
|
|
setupAuthMeAdmin();
|
|
vi.mocked(client.authRuntimeConfig).mockResolvedValue({
|
|
CERTCTL_AUTH_TYPE: 'oidc',
|
|
CERTCTL_BREAKGLASS_ENABLED: 'false',
|
|
CERTCTL_TRUSTED_PROXIES_COUNT: '2',
|
|
});
|
|
|
|
renderWithProviders(<AuthSettingsPage />);
|
|
await waitFor(() => screen.getByTestId('auth-settings-runtime-config'));
|
|
|
|
const panel = screen.getByTestId('auth-settings-runtime-config');
|
|
expect(panel.textContent).toContain('CERTCTL_AUTH_TYPE');
|
|
expect(panel.textContent).toContain('oidc');
|
|
expect(panel.textContent).toContain('CERTCTL_BREAKGLASS_ENABLED');
|
|
expect(panel.textContent).toContain('false');
|
|
expect(panel.textContent).toContain('CERTCTL_TRUSTED_PROXIES_COUNT');
|
|
expect(panel.textContent).toContain('2');
|
|
});
|
|
|
|
it('sorts rows alphabetically by key (stable correlation with log scraping)', async () => {
|
|
setupAuthMeAdmin();
|
|
vi.mocked(client.authRuntimeConfig).mockResolvedValue({
|
|
// Intentionally out of order — the sort comparator should normalize.
|
|
CERTCTL_TRUSTED_PROXIES_COUNT: '0',
|
|
CERTCTL_AUTH_TYPE: 'api-key',
|
|
CERTCTL_BREAKGLASS_ENABLED: 'true',
|
|
});
|
|
|
|
renderWithProviders(<AuthSettingsPage />);
|
|
await waitFor(() => screen.getByTestId('auth-settings-runtime-config'));
|
|
|
|
const panel = screen.getByTestId('auth-settings-runtime-config');
|
|
const auth = panel.textContent!.indexOf('CERTCTL_AUTH_TYPE');
|
|
const bg = panel.textContent!.indexOf('CERTCTL_BREAKGLASS_ENABLED');
|
|
const tp = panel.textContent!.indexOf('CERTCTL_TRUSTED_PROXIES_COUNT');
|
|
expect(auth).toBeGreaterThan(-1);
|
|
expect(bg).toBeGreaterThan(auth);
|
|
expect(tp).toBeGreaterThan(bg);
|
|
});
|
|
|
|
it('empty value renders the "(empty)" placeholder, not a blank cell', async () => {
|
|
setupAuthMeAdmin();
|
|
vi.mocked(client.authRuntimeConfig).mockResolvedValue({
|
|
CERTCTL_BOOTSTRAP_OIDC_PROVIDER_ID: '',
|
|
});
|
|
|
|
renderWithProviders(<AuthSettingsPage />);
|
|
await waitFor(() => screen.getByTestId('auth-settings-runtime-config'));
|
|
|
|
expect(screen.getByTestId('auth-settings-runtime-config').textContent)
|
|
.toContain('(empty)');
|
|
});
|
|
|
|
it('rejected runtime-config query hides the panel silently (e.g. 403 for non-admins)', async () => {
|
|
setupAuthMeAdmin();
|
|
vi.mocked(client.authRuntimeConfig).mockRejectedValue(new Error('HTTP 403: forbidden'));
|
|
|
|
renderWithProviders(<AuthSettingsPage />);
|
|
// Wait for the identity surface so we know render completed.
|
|
await waitFor(() => screen.getByTestId('auth-settings-roles'));
|
|
|
|
// Panel never renders — non-admins must not see the shell of a
|
|
// surface they can't read.
|
|
expect(screen.queryByTestId('auth-settings-runtime-config')).toBeNull();
|
|
});
|
|
});
|