Files
replypier/e2e/specs/widget/token-auth.spec.ts
T
WangShen007 308332eb94 docs: 交付文档、维护指引与端到端回归
补齐 README、开发计划、维护交接与 E2E 用例,执行最终验收。
2026-10-21 21:40:00 +08:00

62 lines
3.3 KiB
TypeScript

import { test, expect, ApiClient, createWidgetApp, issueConversationToken, unique, AI_REPLY } from '@e2e'
test.describe('token-authenticated widget', () => {
test.use({ agentSettings: { requireTokenAuth: true } })
test('a visitor with a token from the site\'s server can chat @widget', async ({ agent, widget, llm, adminApi }) => {
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
const token = await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer)
await llm.script(agent, (s) => s.otherwise().reply('Welcome back!'))
await widget.embed({ token })
await widget.open()
await widget.send('Hi')
await expect(widget.lastAgentMessage()).toHaveText('Welcome back!', AI_REPLY)
})
test('without a token the widget refuses to load @widget', async ({ widget }) => {
await widget.embed()
// The notice lives in the chat panel, so the visitor sees it when they click the launcher.
await widget.launcher.click()
await expect(widget.unavailableNotice).toBeVisible()
})
test('a tampered token is refused @widget @security', async ({ widget, adminApi }) => {
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
const token = await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer)
// Swap the signature for another token's: a well-formed JWT this widget never signed.
const other = await issueConversationToken(adminApi, apiKey, widget.widgetId, { ...widget.customer, email: `x.${widget.customer.email}` })
const tampered = `${token.split('.').slice(0, 2).join('.')}.${other.split('.')[2]}`
await widget.embed({ token: tampered })
await widget.launcher.click()
await expect(widget.unavailableNotice).toBeVisible()
})
test('a token minted for another widget is refused @widget @security', async ({ widget, adminApi, agent, makeWidget }) => {
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
const otherWidget = await makeWidget(agent.id)
const tokenForOther = await issueConversationToken(adminApi, apiKey, otherWidget.widgetId, widget.customer)
await widget.embed({ token: tokenForOther })
await widget.launcher.click()
await expect(widget.unavailableNotice).toBeVisible()
})
test('an expired token no longer opens the chat @widget @security @slow', async ({ widget, adminApi, request }) => {
test.slow()
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
const token = await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer, 60)
// What the loader fetches to render the chat frame for this visitor. Each
// poll on a fresh connection: the backend drops a keep-alive connection
// idle for 5s, the poll interval, and a reused one can hang up mid-request.
const visitor = new ApiClient(request, token)
const frame = () => visitor.raw('GET', `/widgets/${widget.widgetId}/data`, { headers: { Connection: 'close' } })
expect((await frame()).status()).toBe(200)
// The shortest lifetime the API allows; the backend's own clock decides.
await expect.poll(async () => (await frame()).status(), { timeout: 90_000, intervals: [5_000] }).toBe(401)
expect(JSON.stringify(await (await frame()).json())).toContain('Conversation token expired')
})
})