mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
d559fdda1d
Minting a key for other software meant finding it under MCP, behind a heading about AI assistants and gated on an addon it does not need. It now sits on its own in Settings, above the MCP block, and works on an instance with MCP switched off. Deliberately separate state rather than another branch of the tab's shared hook: the section has to stand on its own, and that hook is already carrying forty fields through three levels of props. Translated into all 23 languages.
229 lines
8.3 KiB
TypeScript
229 lines
8.3 KiB
TypeScript
// 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(
|
|
<>
|
|
<ApiKeysSection />
|
|
<ToastContainer />
|
|
</>,
|
|
);
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|