62 lines
3.3 KiB
TypeScript
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')
|
|
})
|
|
})
|