docs: 交付文档、维护指引与端到端回归
补齐 README、开发计划、维护交接与 E2E 用例,执行最终验收。
This commit is contained in:
1 parent
18732801b5
commit
6cd904d460
216 files changed
+37038
No files matched your search
@@ -0,0 +1,61 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user