Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions app/src/pages/DebugPage.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(<DebugPage />);

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(<DebugPage />);

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<string, string> | 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(<DebugPage />);

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');
});
Comment on lines +115 to +147
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "submits the token…" test writes anyplot.adminToken into sessionStorage but doesn’t clean it up afterwards. This makes the file’s tests order-dependent (a later added test could accidentally start with a token already present). Consider clearing sessionStorage in a shared beforeEach/afterEach, or explicitly removing the key at the end of this test.

Copilot uses AI. Check for mistakes.

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(<DebugPage />);

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();
});

});
4 changes: 1 addition & 3 deletions core/database/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
Loading