Wire ApiTokensPanel after the profile form, add the two-step create dialog, phrase keys, smoke tracer, and rebuilt boardwalk dist. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { enableAutoUnmount, flushPromises } from '@vue/test-utils'
|
|
import { API, mountApp, requestsTo, resetState, type Route } from '../helpers'
|
|
|
|
const TOKENS = `${API}/account/tokens`
|
|
const SCOPES = `${API}/account/tokens/scopes`
|
|
|
|
const envelope = (data: unknown, status = 200) => ({ status, body: { data, meta: {} } })
|
|
|
|
const existingToken = {
|
|
id: 11,
|
|
name: 'Blog agent',
|
|
hint: 'a1b2',
|
|
scopes: [
|
|
{
|
|
name: 'admin:read',
|
|
label: 'Read-only, whole admin',
|
|
description: 'View everything',
|
|
read_only: true,
|
|
known: true,
|
|
},
|
|
],
|
|
created_at: '2026-01-10T12:00:00Z',
|
|
last_used_at: '',
|
|
expires_at: '2026-04-10T12:00:00Z',
|
|
revoked_at: '',
|
|
expired: false,
|
|
}
|
|
|
|
const createdToken = {
|
|
...existingToken,
|
|
id: 12,
|
|
name: 'CI runner',
|
|
hint: 'c3d4',
|
|
token: 'g15_0123456789abcdef0123456789abcdef01234567',
|
|
}
|
|
|
|
const scopes = [
|
|
{
|
|
name: '*',
|
|
label: 'Full access',
|
|
description: 'Everything',
|
|
read_only: false,
|
|
known: true,
|
|
},
|
|
{
|
|
name: 'admin:read',
|
|
label: 'Read-only, whole admin',
|
|
description: 'View everything',
|
|
read_only: true,
|
|
known: true,
|
|
},
|
|
]
|
|
|
|
function tokenRoutes(overrides: Record<string, Route> = {}, list: unknown[] = [existingToken]): Record<string, Route> {
|
|
let listed = list
|
|
return {
|
|
[`GET ${TOKENS}`]: () => envelope(listed),
|
|
[`GET ${SCOPES}`]: envelope(scopes),
|
|
[`POST ${TOKENS}`]: () => {
|
|
listed = [createdToken, ...listed]
|
|
return envelope(createdToken, 201)
|
|
},
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
async function openAccount(routes: Record<string, Route> = {}, list: unknown[] = [existingToken]) {
|
|
return mountApp('/account', tokenRoutes(routes, list), { attach: true })
|
|
}
|
|
|
|
const panel = () => document.body.querySelector<HTMLElement>('[data-api-tokens]')
|
|
const dialog = () => document.body.querySelector<HTMLElement>('[data-token-dialog]')
|
|
|
|
async function click(selector: string, root: ParentNode = document.body): Promise<void> {
|
|
const el = root.querySelector<HTMLElement>(selector)
|
|
el?.click()
|
|
await flushPromises()
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetState()
|
|
})
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = ''
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
describe('ApiTokensPanel', () => {
|
|
it('tracer: list, create, show once, hide', async () => {
|
|
const { calls } = await openAccount()
|
|
await flushPromises()
|
|
expect(panel()).toBeTruthy()
|
|
expect(panel()!.querySelector('[data-token-row="11"]')).toBeTruthy()
|
|
|
|
await click('[data-action="create-token"]', panel()!)
|
|
expect(dialog()).toBeTruthy()
|
|
|
|
const nameInput = dialog()!.querySelector<HTMLInputElement>('#api-token-name')!
|
|
await nameInput.focus()
|
|
nameInput.value = 'CI runner'
|
|
nameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
await flushPromises()
|
|
|
|
const scope = dialog()!.querySelector<HTMLInputElement>('[data-scope="admin:read"] input')!
|
|
scope.checked = true
|
|
scope.dispatchEvent(new Event('change', { bubbles: true }))
|
|
await flushPromises()
|
|
|
|
const getsBeforeCreate = requestsTo(calls, 'GET', TOKENS).length
|
|
await click('[data-action="create-token-submit"]', dialog()!)
|
|
const [post] = requestsTo(calls, 'POST', TOKENS)
|
|
expect(post).toBeTruthy()
|
|
expect(await post!.clone().json()).toEqual({
|
|
name: 'CI runner',
|
|
expires: '90d',
|
|
scopes: ['admin:read'],
|
|
})
|
|
|
|
const plaintext = dialog()!.querySelector<HTMLInputElement>('[data-token-plaintext]')!
|
|
expect(plaintext.value).toBe(createdToken.token)
|
|
expect(requestsTo(calls, 'GET', TOKENS).length).toBeGreaterThan(getsBeforeCreate)
|
|
|
|
await click('[data-action="hide-token"]', dialog()!)
|
|
expect(dialog()).toBeNull()
|
|
expect(document.body.textContent).not.toContain(createdToken.token)
|
|
})
|
|
})
|