mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
e3c524dd02
SonarQube's S2699 flagged 196 test cases as assertion-less. Almost all of them end on a bare `await screen.findByText(...)`, which does throw when the text never shows up — the test is real, the analyser just cannot see it. Spelling the check out keeps the behaviour identical and makes it readable. Writing them out turned three tests red, each for a good reason: - FE-COMP-NOTES-008 read the heading out of the loading panel, which renders its own copy and swaps the whole node out once the fetch settles. It now waits for the loaded state. - FE-COMP-PHOTOPROVIDERS-014 matched /connected/i, which also matches the "Not connected" the badge starts as — the test passed whether or not the button did anything. Exact text now. - FE-FILES-002 compared an empty list against the empty list it already was. It seeds a file first, so the 500 has something to leave alone. JOURNEY-SVC-066 asserted nothing at all behind a "should not throw" comment; it now says that out loud and checks no journey entry is left orphaned.
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
// FE-ADMIN-MCP-001 to FE-ADMIN-MCP-016
|
|
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { server } from '../../../tests/helpers/msw/server';
|
|
import { resetAllStores } from '../../../tests/helpers/store';
|
|
import { ToastContainer } from '../shared/Toast';
|
|
import AdminMcpTokensPanel from './AdminMcpTokensPanel';
|
|
|
|
const TOKEN_1 = {
|
|
id: 1,
|
|
name: 'CI Token',
|
|
token_prefix: 'trek_abc',
|
|
created_at: '2025-01-15T00:00:00Z',
|
|
last_used_at: null,
|
|
user_id: 10,
|
|
username: 'alice',
|
|
};
|
|
|
|
const TOKEN_2 = {
|
|
id: 2,
|
|
name: 'Ops Token',
|
|
token_prefix: 'trek_xyz',
|
|
created_at: '2025-03-01T00:00:00Z',
|
|
last_used_at: '2025-04-01T00:00:00Z',
|
|
user_id: 11,
|
|
username: 'bob',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.resetHandlers();
|
|
});
|
|
|
|
describe('AdminMcpTokensPanel', () => {
|
|
it('FE-ADMIN-MCP-001: loading spinner shown on mount', async () => {
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
return HttpResponse.json({ tokens: [] });
|
|
})
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-002: empty state rendered when no tokens', async () => {
|
|
render(<AdminMcpTokensPanel />);
|
|
expect(await screen.findByText('No MCP tokens have been created yet')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-003: token list renders correctly', async () => {
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ tokens: [TOKEN_1, TOKEN_2] })
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
await screen.findByText('CI Token');
|
|
expect(screen.getByText('Ops Token')).toBeInTheDocument();
|
|
expect(screen.getByText('alice')).toBeInTheDocument();
|
|
expect(screen.getByText('bob')).toBeInTheDocument();
|
|
// token_prefix is rendered as `{token.token_prefix}...` — two adjacent text nodes
|
|
expect(screen.getByText(/trek_abc/)).toBeInTheDocument();
|
|
expect(screen.getByText(/trek_xyz/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-004: "Never" shown when last_used_at is null', async () => {
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ tokens: [TOKEN_1, TOKEN_2] })
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
await screen.findByText('CI Token');
|
|
expect(screen.getByText('Never')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-005: delete confirmation dialog opens', async () => {
|
|
const user = userEvent.setup();
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ tokens: [TOKEN_1, TOKEN_2] })
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
await screen.findByText('CI Token');
|
|
|
|
const deleteButtons = screen.getAllByTitle('Delete');
|
|
await user.click(deleteButtons[0]);
|
|
|
|
expect(screen.getByText('Delete Token')).toBeInTheDocument();
|
|
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
// Dialog Delete button has visible text "Delete"; trash icon buttons have no text content
|
|
expect(screen.getByText('Delete')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-006: cancel closes confirmation dialog without deleting', async () => {
|
|
const user = userEvent.setup();
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ tokens: [TOKEN_1, TOKEN_2] })
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
await screen.findByText('CI Token');
|
|
|
|
const deleteButtons = screen.getAllByTitle('Delete');
|
|
await user.click(deleteButtons[0]);
|
|
expect(screen.getByText('Delete Token')).toBeInTheDocument();
|
|
|
|
await user.click(screen.getByText('Cancel'));
|
|
|
|
expect(screen.queryByText('Delete Token')).not.toBeInTheDocument();
|
|
expect(screen.getByText('CI Token')).toBeInTheDocument();
|
|
expect(screen.getByText('Ops Token')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-007: backdrop click closes dialog', async () => {
|
|
const user = userEvent.setup();
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ tokens: [TOKEN_1, TOKEN_2] })
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
await screen.findByText('CI Token');
|
|
|
|
const deleteButtons = screen.getAllByTitle('Delete');
|
|
await user.click(deleteButtons[0]);
|
|
expect(screen.getByText('Delete Token')).toBeInTheDocument();
|
|
|
|
const backdrop = document.querySelector('.fixed.inset-0');
|
|
expect(backdrop).toBeInTheDocument();
|
|
await user.click(backdrop!);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Delete Token')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-008: successful delete removes token from list', async () => {
|
|
const user = userEvent.setup();
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ tokens: [TOKEN_1, TOKEN_2] })
|
|
),
|
|
http.delete('/api/admin/mcp-tokens/:id', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
render(<><ToastContainer /><AdminMcpTokensPanel /></>);
|
|
await screen.findByText('CI Token');
|
|
|
|
const deleteButtons = screen.getAllByTitle('Delete');
|
|
await user.click(deleteButtons[0]);
|
|
await user.click(screen.getByText('Delete'));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Delete Token')).not.toBeInTheDocument();
|
|
});
|
|
expect(screen.queryByText('CI Token')).not.toBeInTheDocument();
|
|
expect(screen.getByText('Ops Token')).toBeInTheDocument();
|
|
await screen.findByText('Token deleted');
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-009: failed delete shows error toast and keeps list unchanged', async () => {
|
|
const user = userEvent.setup();
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ tokens: [TOKEN_1, TOKEN_2] })
|
|
),
|
|
http.delete('/api/admin/mcp-tokens/:id', () =>
|
|
HttpResponse.json({ error: 'forbidden' }, { status: 403 })
|
|
)
|
|
);
|
|
render(<><ToastContainer /><AdminMcpTokensPanel /></>);
|
|
await screen.findByText('CI Token');
|
|
|
|
const deleteButtons = screen.getAllByTitle('Delete');
|
|
await user.click(deleteButtons[0]);
|
|
await user.click(screen.getByText('Delete'));
|
|
|
|
await screen.findByText('Failed to delete token');
|
|
expect(screen.getByText('CI Token')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-010: load failure shows error toast', async () => {
|
|
server.use(
|
|
http.get('/api/admin/mcp-tokens', () =>
|
|
HttpResponse.json({ error: 'server error' }, { status: 500 })
|
|
)
|
|
);
|
|
render(<><ToastContainer /><AdminMcpTokensPanel /></>);
|
|
expect(await screen.findByText('Failed to load tokens')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-011: OAuth sessions loading spinner shown on mount', async () => {
|
|
server.use(
|
|
http.get('/api/admin/oauth-sessions', async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
return HttpResponse.json({ sessions: [] });
|
|
})
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-012: OAuth sessions empty state rendered when no sessions', async () => {
|
|
server.use(
|
|
http.get('/api/admin/oauth-sessions', () =>
|
|
HttpResponse.json({ sessions: [] })
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
expect(await screen.findByText('No active OAuth sessions')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-013: OAuth sessions list renders with scopes', async () => {
|
|
server.use(
|
|
http.get('/api/admin/oauth-sessions', () =>
|
|
HttpResponse.json({
|
|
sessions: [
|
|
{
|
|
id: 1,
|
|
client_name: 'Claude Desktop',
|
|
username: 'alice',
|
|
scopes: ['trips:read', 'budget:read'],
|
|
created_at: '2025-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
})
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
await screen.findByText('Claude Desktop');
|
|
expect(screen.getByText('alice')).toBeInTheDocument();
|
|
expect(screen.getByText('trips:read')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-014: scope expand/collapse toggle shows hidden scopes', async () => {
|
|
const user = userEvent.setup();
|
|
// 7 scopes — more than SCOPES_PREVIEW=6, so "+1 more" button appears
|
|
const scopes = ['trips:read', 'trips:write', 'places:read', 'places:write', 'budget:read', 'budget:write', 'packing:read'];
|
|
server.use(
|
|
http.get('/api/admin/oauth-sessions', () =>
|
|
HttpResponse.json({
|
|
sessions: [
|
|
{ id: 1, client_name: 'App', username: 'bob', scopes, created_at: '2025-01-01T00:00:00Z' },
|
|
],
|
|
})
|
|
)
|
|
);
|
|
render(<AdminMcpTokensPanel />);
|
|
await screen.findByText('App');
|
|
// "+1 more" button should appear
|
|
const moreBtn = await screen.findByText(/\+1 more/);
|
|
expect(moreBtn).toBeInTheDocument();
|
|
await user.click(moreBtn);
|
|
// After expand, "show less" appears
|
|
expect(await screen.findByText('show less')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-015: revoke session confirmation and successful revoke', async () => {
|
|
const user = userEvent.setup();
|
|
server.use(
|
|
http.get('/api/admin/oauth-sessions', () =>
|
|
HttpResponse.json({
|
|
sessions: [
|
|
{ id: 5, client_name: 'Revoke Me', username: 'carol', scopes: ['trips:read'], created_at: '2025-01-01T00:00:00Z' },
|
|
],
|
|
})
|
|
),
|
|
http.delete('/api/admin/oauth-sessions/5', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
render(<><ToastContainer /><AdminMcpTokensPanel /></>);
|
|
await screen.findByText('Revoke Me');
|
|
|
|
// Click the revoke (trash) button next to the session
|
|
const deleteBtn = screen.getAllByTitle('Delete')[0];
|
|
await user.click(deleteBtn);
|
|
|
|
// Confirmation modal opens
|
|
expect(screen.getByText('Revoke Session')).toBeInTheDocument();
|
|
// Confirm — find the modal's Delete button (has no title, unlike the trash icon)
|
|
const deleteBtns = screen.getAllByRole('button', { name: 'Delete' });
|
|
const confirmBtn = deleteBtns.find(b => !b.title);
|
|
await user.click(confirmBtn ?? deleteBtns[deleteBtns.length - 1]);
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Revoke Me')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('FE-ADMIN-MCP-016: revoke session error shows toast', async () => {
|
|
const user = userEvent.setup();
|
|
server.use(
|
|
http.get('/api/admin/oauth-sessions', () =>
|
|
HttpResponse.json({
|
|
sessions: [
|
|
{ id: 6, client_name: 'Error Session', username: 'dave', scopes: ['trips:read'], created_at: '2025-01-01T00:00:00Z' },
|
|
],
|
|
})
|
|
),
|
|
http.delete('/api/admin/oauth-sessions/6', () =>
|
|
HttpResponse.json({ error: 'forbidden' }, { status: 403 })
|
|
)
|
|
);
|
|
render(<><ToastContainer /><AdminMcpTokensPanel /></>);
|
|
await screen.findByText('Error Session');
|
|
|
|
const deleteBtn = screen.getAllByTitle('Delete')[0];
|
|
await user.click(deleteBtn);
|
|
const deleteBtns = screen.getAllByRole('button', { name: 'Delete' });
|
|
const confirmBtn = deleteBtns.find(b => !b.title);
|
|
await user.click(confirmBtn ?? deleteBtns[deleteBtns.length - 1]);
|
|
expect(await screen.findByText('Failed to revoke session')).toBeInTheDocument();
|
|
});
|
|
});
|