Files
Mangalord/frontend/e2e/profile-tokens.spec.ts
MechaCat02 1ed1a134ea feat: bot API token management page
The account page pointed users at a "bot-token list" that didn't exist: there
was no GET endpoint, no UI route, and the client type lacked expiry. Add
GET /v1/auth/tokens (caller-scoped, token_hash never serialised), extend the
auth client with listTokens/expires_at and createToken expiry, and add a
/profile/tokens page to list, create (revealing the raw bearer once), and
revoke tokens. Link the account-page copy to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:55:24 +02:00

95 lines
3.5 KiB
TypeScript

import { test, expect, type Page } from './fixtures';
// E2E for the bot API token management page: list existing tokens, create one
// (the raw bearer is shown once), and revoke one.
const existing = {
id: 't1111111-1111-1111-1111-111111111111',
user_id: 'u1',
name: 'existing-bot',
created_at: '2026-01-01T00:00:00Z',
last_used_at: null,
expires_at: null
};
type Captured = { created: Record<string, unknown> | null; deleted: string | null };
async function mockTokens(page: Page, cap: Captured) {
await page.route('**/api/v1/auth/config', (r) =>
r.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ self_register_enabled: true, private_mode: false })
})
);
await page.route('**/api/v1/auth/me', (r) =>
r.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
user: { id: 'u1', username: 'reader', created_at: '2026-01-01T00:00:00Z', is_admin: false }
})
})
);
await page.route('**/api/v1/auth/me/preferences', (r) =>
r.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
);
await page.route('**/api/v1/me/bookmarks*', (r) =>
r.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } })
})
);
// Specific id-scoped DELETE first (last-match-wins ordering).
await page.route('**/api/v1/auth/tokens/*', async (r) => {
cap.deleted = r.request().url().split('/').pop() ?? null;
await r.fulfill({ status: 204, body: '' });
});
await page.route('**/api/v1/auth/tokens', async (r) => {
if (r.request().method() === 'POST') {
cap.created = r.request().postDataJSON();
await r.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
id: 't2',
user_id: 'u1',
name: cap.created?.name,
created_at: '2026-03-01T00:00:00Z',
last_used_at: null,
expires_at: null,
bearer: 'secret-raw-bearer-xyz'
})
});
return;
}
await r.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [existing] })
});
});
}
test('lists, creates (shows the bearer once), and revokes tokens', async ({ page }) => {
const cap: Captured = { created: null, deleted: null };
await mockTokens(page, cap);
await page.goto('/profile/tokens');
// Existing token is listed.
await expect(page.getByTestId(`token-row-${existing.id}`)).toContainText('existing-bot');
// Create a token — the raw bearer is revealed exactly once.
await page.getByTestId('token-name').fill('new-bot');
await page.getByRole('button', { name: 'Create token' }).click();
await expect(page.getByTestId('token-fresh')).toContainText('secret-raw-bearer-xyz');
await expect.poll(() => cap.created).toEqual({ name: 'new-bot' });
// Revoke the existing token (confirm dialog accepted).
page.on('dialog', (d) => d.accept());
await page.getByTestId(`token-revoke-${existing.id}`).click();
await expect.poll(() => cap.deleted).toBe(existing.id);
});