// FE-COMP-APIKEYS-001 to FE-COMP-APIKEYS-010
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 { useAuthStore } from '../../store/authStore';
import { useAddonStore } from '../../store/addonStore';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { buildUser } from '../../../tests/helpers/factories';
import { ToastContainer } from '../shared/Toast';
import ApiKeysSection from './ApiKeysSection';
const clipboardWriteText = vi.fn().mockResolvedValue(undefined);
beforeAll(() => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: clipboardWriteText },
configurable: true,
writable: true,
});
});
beforeEach(() => {
clipboardWriteText.mockClear();
resetAllStores();
vi.clearAllMocks();
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
seedStore(useAddonStore, { addons: [], loaded: true, loadAddons: vi.fn() });
server.use(http.get('/api/auth/api-tokens', () => HttpResponse.json({ tokens: [] })));
});
function renderSection() {
return render(
<>
>,
);
}
describe('ApiKeysSection', () => {
it('FE-COMP-APIKEYS-001: renders without the MCP addon, because an API key does not need it', async () => {
renderSection();
expect(await screen.findByText('API Keys')).toBeInTheDocument();
});
it('FE-COMP-APIKEYS-002: reads its own endpoint, not the MCP token list', async () => {
const seen: string[] = [];
server.use(
http.get('/api/auth/api-tokens', ({ request }) => {
seen.push(new URL(request.url).pathname);
return HttpResponse.json({ tokens: [] });
}),
http.get('/api/auth/mcp-tokens', ({ request }) => {
seen.push(new URL(request.url).pathname);
return HttpResponse.json({ tokens: [] });
}),
);
renderSection();
await waitFor(() => expect(seen).toContain('/api/auth/api-tokens'));
expect(seen).not.toContain('/api/auth/mcp-tokens');
});
it('FE-COMP-APIKEYS-003: lists existing keys by prefix, never the key itself', async () => {
server.use(
http.get('/api/auth/api-tokens', () =>
HttpResponse.json({
tokens: [
{
id: 7,
name: 'Dawarich',
token_prefix: 'trek_abcdefg',
created_at: '2026-08-01T10:00:00Z',
last_used_at: null,
},
],
}),
),
);
renderSection();
expect(await screen.findByText('Dawarich')).toBeInTheDocument();
expect(screen.getByText(/trek_abcdefg\.\.\./)).toBeInTheDocument();
});
it('FE-COMP-APIKEYS-004: shows the empty state when there is nothing to list', async () => {
renderSection();
expect(await screen.findByText(/No keys yet/)).toBeInTheDocument();
});
it('FE-COMP-APIKEYS-005: creates a key and shows it once, with the warning', async () => {
server.use(
http.post('/api/auth/api-tokens', () =>
HttpResponse.json({
token: {
id: 1,
name: 'Dawarich',
raw_token: 'trek_thefullsecretvalue',
token_prefix: 'trek_thefull',
created_at: '2026-08-27T10:00:00Z',
},
}),
),
);
const user = userEvent.setup();
renderSection();
await user.click(await screen.findByRole('button', { name: /Create key/ }));
await user.type(screen.getByPlaceholderText('e.g. Dawarich'), 'Dawarich');
await user.click(screen.getByRole('button', { name: /^Create$/ }));
expect(await screen.findByText('trek_thefullsecretvalue')).toBeInTheDocument();
expect(screen.getByText(/shown once/)).toBeInTheDocument();
});
it('FE-COMP-APIKEYS-006: drops the raw key from the DOM once the modal is closed', async () => {
server.use(
http.post('/api/auth/api-tokens', () =>
HttpResponse.json({
token: {
id: 1,
name: 'Dawarich',
raw_token: 'trek_thefullsecretvalue',
token_prefix: 'trek_thefull',
created_at: '2026-08-27T10:00:00Z',
},
}),
),
);
const user = userEvent.setup();
renderSection();
await user.click(await screen.findByRole('button', { name: /Create key/ }));
await user.type(screen.getByPlaceholderText('e.g. Dawarich'), 'Dawarich');
await user.click(screen.getByRole('button', { name: /^Create$/ }));
await screen.findByText('trek_thefullsecretvalue');
await user.click(screen.getByRole('button', { name: /Done/ }));
await waitFor(() => expect(screen.queryByText('trek_thefullsecretvalue')).not.toBeInTheDocument());
// The list keeps only the prefix, which is what the server stores alongside the hash.
expect(screen.getByText(/trek_thefull\.\.\./)).toBeInTheDocument();
});
it('FE-COMP-APIKEYS-007: refuses to create a key without a name', async () => {
const user = userEvent.setup();
renderSection();
await user.click(await screen.findByRole('button', { name: /Create key/ }));
expect(screen.getByRole('button', { name: /^Create$/ })).toBeDisabled();
});
it('FE-COMP-APIKEYS-008: asks before deleting, and only deletes on confirm', async () => {
let deleted = 0;
server.use(
http.get('/api/auth/api-tokens', () =>
HttpResponse.json({
tokens: [
{ id: 7, name: 'Dawarich', token_prefix: 'trek_abc', created_at: '2026-08-01T10:00:00Z', last_used_at: null },
],
}),
),
http.delete('/api/auth/api-tokens/7', () => {
deleted += 1;
return HttpResponse.json({ success: true });
}),
);
const user = userEvent.setup();
renderSection();
await user.click(await screen.findByTitle('Delete key'));
expect(deleted).toBe(0);
expect(screen.getByText(/stops working immediately/)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /^Cancel$/ }));
expect(deleted).toBe(0);
await user.click(await screen.findByTitle('Delete key'));
const [, confirm] = screen.getAllByRole('button', { name: /Delete key/ });
await user.click(confirm);
await waitFor(() => expect(deleted).toBe(1));
await waitFor(() => expect(screen.queryByText('Dawarich')).not.toBeInTheDocument());
});
it('FE-COMP-APIKEYS-009: reports a failed creation instead of pretending it worked', async () => {
server.use(http.post('/api/auth/api-tokens', () => HttpResponse.json({ error: 'nope' }, { status: 500 })));
const user = userEvent.setup();
renderSection();
await user.click(await screen.findByRole('button', { name: /Create key/ }));
await user.type(screen.getByPlaceholderText('e.g. Dawarich'), 'Dawarich');
await user.click(screen.getByRole('button', { name: /^Create$/ }));
expect(await screen.findByText(/Could not create the key/)).toBeInTheDocument();
});
it('FE-COMP-APIKEYS-010: copies the new key to the clipboard on request', async () => {
server.use(
http.post('/api/auth/api-tokens', () =>
HttpResponse.json({
token: {
id: 1,
name: 'Dawarich',
raw_token: 'trek_thefullsecretvalue',
token_prefix: 'trek_thefull',
created_at: '2026-08-27T10:00:00Z',
},
}),
),
);
const user = userEvent.setup();
// userEvent.setup() installs its own clipboard stub, so the spy goes back on
// afterwards — otherwise this asserts against a stub the component never reaches.
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: clipboardWriteText },
configurable: true,
writable: true,
});
renderSection();
await user.click(await screen.findByRole('button', { name: /Create key/ }));
await user.type(screen.getByPlaceholderText('e.g. Dawarich'), 'Dawarich');
await user.click(screen.getByRole('button', { name: /^Create$/ }));
await screen.findByText('trek_thefullsecretvalue');
await user.click(screen.getByTitle('Copy'));
expect(clipboardWriteText).toHaveBeenCalledWith('trek_thefullsecretvalue');
// And the button confirms it to the reader, which is the only feedback there is.
expect(await screen.findByTitle('Copy')).toBeInTheDocument();
});
});