Skip to content

Commit 5decb9d

Browse files
chore(audit): fix CI — ruff format + DebugPage auth-branch coverage
CI on 4d3e5d3 failed two checks: 1. **Run Linting** — ruff format wanted `core/database/repositories.py` collapsed onto one line (the multi-line call fit within line-length after my get_all() simplification). 2. **codecov/patch/frontend** — 65.38% diff coverage (target 80%). The admin-token branch in `DebugPage.tsx` (401/503 → token form, submit flow, sessionStorage persistence, clear) had no tests. Added 4 cases to `DebugPage.test.tsx`: - 401 renders the token form with the "admin token required" hint - 503 renders it with the "not configured" hint - submit flow sends `X-Admin-Token` and persists to sessionStorage - clear-stored-token wipes sessionStorage and re-prompts Imports `fireEvent` from `@testing-library/react` directly because `test-utils.tsx` doesn't re-export it. ## Verification - `uv run ruff format --check .` — clean - `cd app && yarn tsc --noEmit` — clean - `cd app && yarn test --run` — 396 tests pass (was 392, +4) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 46b3bad commit 5decb9d

2 files changed

Lines changed: 80 additions & 3 deletions

File tree

app/src/pages/DebugPage.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { fireEvent } from '@testing-library/react';
23
import { render, screen, waitFor } from '../test-utils';
34

45
import { DebugPage } from './DebugPage';
@@ -83,4 +84,82 @@ describe('DebugPage', () => {
8384
});
8485
});
8586

87+
// /debug routes are gated by `require_admin` (api/routers/debug.py). The
88+
// browser handles the gate via an X-Admin-Token sessionStorage UX; these
89+
// tests cover the 401/503 branch + submit/clear flow that the existing
90+
// tests miss (and the codecov/patch gate flagged at 65% diff coverage).
91+
it('renders the admin-token form on 401', async () => {
92+
sessionStorage.clear();
93+
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: false, status: 401 })));
94+
95+
render(<DebugPage />);
96+
97+
await waitFor(() => {
98+
expect(screen.getByPlaceholderText('X-Admin-Token')).toBeInTheDocument();
99+
});
100+
expect(screen.getByRole('button', { name: /unlock/i })).toBeInTheDocument();
101+
expect(screen.getByText(/admin token required/i)).toBeInTheDocument();
102+
});
103+
104+
it('renders the admin-token form on 503 with the not-configured hint', async () => {
105+
sessionStorage.clear();
106+
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: false, status: 503 })));
107+
108+
render(<DebugPage />);
109+
110+
await waitFor(() => {
111+
expect(screen.getByText(/admin auth not configured/i)).toBeInTheDocument();
112+
});
113+
});
114+
115+
it('submits the token, sends X-Admin-Token, and persists to sessionStorage', async () => {
116+
sessionStorage.clear();
117+
let callIndex = 0;
118+
const fetchMock = vi.fn((url: string, init?: RequestInit) => {
119+
callIndex += 1;
120+
// First /debug/status → 401 (no token yet); subsequent calls → ok.
121+
if (url.includes('/debug/ping')) {
122+
return Promise.resolve({
123+
ok: true,
124+
json: () => Promise.resolve({ database_connected: true, response_time_ms: 10, timestamp: '2026-04-27T00:00:00Z' }),
125+
});
126+
}
127+
if (callIndex === 1) {
128+
return Promise.resolve({ ok: false, status: 401 });
129+
}
130+
// Verify the header is sent on the retry.
131+
const hdr = (init?.headers as Record<string, string> | undefined)?.['X-Admin-Token'];
132+
if (hdr !== 'secret-xyz') return Promise.resolve({ ok: false, status: 401 });
133+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDebugData) });
134+
});
135+
vi.stubGlobal('fetch', fetchMock);
136+
137+
render(<DebugPage />);
138+
139+
const input = await screen.findByPlaceholderText('X-Admin-Token');
140+
fireEvent.change(input, { target: { value: 'secret-xyz' } });
141+
fireEvent.click(screen.getByRole('button', { name: /unlock/i }));
142+
143+
await waitFor(() => {
144+
expect(screen.getAllByText('scatter-basic').length).toBeGreaterThan(0);
145+
});
146+
expect(sessionStorage.getItem('anyplot.adminToken')).toBe('secret-xyz');
147+
});
148+
149+
it('clears the stored token and re-prompts', async () => {
150+
sessionStorage.setItem('anyplot.adminToken', 'stored-token');
151+
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: false, status: 401 })));
152+
153+
render(<DebugPage />);
154+
155+
const clearBtn = await screen.findByRole('button', { name: /clear stored token/i });
156+
fireEvent.click(clearBtn);
157+
158+
await waitFor(() => {
159+
expect(sessionStorage.getItem('anyplot.adminToken')).toBeNull();
160+
});
161+
// Form is still on screen because fetch still returns 401.
162+
expect(screen.getByPlaceholderText('X-Admin-Token')).toBeInTheDocument();
163+
});
164+
86165
});

core/database/repositories.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,7 @@ async def get_all(self) -> list[Spec]:
161161
session must use `get_all_with_code()` to avoid MissingGreenlet.
162162
"""
163163
impls_loader = selectinload(Spec.impls)
164-
result = await self.session.execute(
165-
select(Spec).options(impls_loader.selectinload(Impl.library))
166-
)
164+
result = await self.session.execute(select(Spec).options(impls_loader.selectinload(Impl.library)))
167165
return list(result.scalars().all())
168166

169167
async def get_all_with_code(self) -> list[Spec]:

0 commit comments

Comments
 (0)