From 5decb9d95165304462e5d597b26b1cd72f1f195a Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:18:46 +0200 Subject: [PATCH] =?UTF-8?q?chore(audit):=20fix=20CI=20=E2=80=94=20ruff=20f?= =?UTF-8?q?ormat=20+=20DebugPage=20auth-branch=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on 4d3e5d3c 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) --- app/src/pages/DebugPage.test.tsx | 79 ++++++++++++++++++++++++++++++++ core/database/repositories.py | 4 +- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/app/src/pages/DebugPage.test.tsx b/app/src/pages/DebugPage.test.tsx index c4b8d92fa9..e25ddb4f0e 100644 --- a/app/src/pages/DebugPage.test.tsx +++ b/app/src/pages/DebugPage.test.tsx @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fireEvent } from '@testing-library/react'; import { render, screen, waitFor } from '../test-utils'; import { DebugPage } from './DebugPage'; @@ -83,4 +84,82 @@ describe('DebugPage', () => { }); }); + // /debug routes are gated by `require_admin` (api/routers/debug.py). The + // browser handles the gate via an X-Admin-Token sessionStorage UX; these + // tests cover the 401/503 branch + submit/clear flow that the existing + // tests miss (and the codecov/patch gate flagged at 65% diff coverage). + it('renders the admin-token form on 401', async () => { + sessionStorage.clear(); + vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: false, status: 401 }))); + + render(); + + await waitFor(() => { + expect(screen.getByPlaceholderText('X-Admin-Token')).toBeInTheDocument(); + }); + expect(screen.getByRole('button', { name: /unlock/i })).toBeInTheDocument(); + expect(screen.getByText(/admin token required/i)).toBeInTheDocument(); + }); + + it('renders the admin-token form on 503 with the not-configured hint', async () => { + sessionStorage.clear(); + vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: false, status: 503 }))); + + render(); + + await waitFor(() => { + expect(screen.getByText(/admin auth not configured/i)).toBeInTheDocument(); + }); + }); + + it('submits the token, sends X-Admin-Token, and persists to sessionStorage', async () => { + sessionStorage.clear(); + let callIndex = 0; + const fetchMock = vi.fn((url: string, init?: RequestInit) => { + callIndex += 1; + // First /debug/status → 401 (no token yet); subsequent calls → ok. + if (url.includes('/debug/ping')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ database_connected: true, response_time_ms: 10, timestamp: '2026-04-27T00:00:00Z' }), + }); + } + if (callIndex === 1) { + return Promise.resolve({ ok: false, status: 401 }); + } + // Verify the header is sent on the retry. + const hdr = (init?.headers as Record | undefined)?.['X-Admin-Token']; + if (hdr !== 'secret-xyz') return Promise.resolve({ ok: false, status: 401 }); + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockDebugData) }); + }); + vi.stubGlobal('fetch', fetchMock); + + render(); + + const input = await screen.findByPlaceholderText('X-Admin-Token'); + fireEvent.change(input, { target: { value: 'secret-xyz' } }); + fireEvent.click(screen.getByRole('button', { name: /unlock/i })); + + await waitFor(() => { + expect(screen.getAllByText('scatter-basic').length).toBeGreaterThan(0); + }); + expect(sessionStorage.getItem('anyplot.adminToken')).toBe('secret-xyz'); + }); + + it('clears the stored token and re-prompts', async () => { + sessionStorage.setItem('anyplot.adminToken', 'stored-token'); + vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: false, status: 401 }))); + + render(); + + const clearBtn = await screen.findByRole('button', { name: /clear stored token/i }); + fireEvent.click(clearBtn); + + await waitFor(() => { + expect(sessionStorage.getItem('anyplot.adminToken')).toBeNull(); + }); + // Form is still on screen because fetch still returns 401. + expect(screen.getByPlaceholderText('X-Admin-Token')).toBeInTheDocument(); + }); + }); diff --git a/core/database/repositories.py b/core/database/repositories.py index cb8c7cddfa..1537b413d9 100644 --- a/core/database/repositories.py +++ b/core/database/repositories.py @@ -161,9 +161,7 @@ async def get_all(self) -> list[Spec]: session must use `get_all_with_code()` to avoid MissingGreenlet. """ impls_loader = selectinload(Spec.impls) - result = await self.session.execute( - select(Spec).options(impls_loader.selectinload(Impl.library)) - ) + result = await self.session.execute(select(Spec).options(impls_loader.selectinload(Impl.library))) return list(result.scalars().all()) async def get_all_with_code(self) -> list[Spec]: