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>
95 lines
3.5 KiB
TypeScript
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);
|
|
});
|