library-picturebook-activity/lesingle-creation-frontend/e2e/leai/webhook-api.spec.ts
En 98e9ad1d28 feat(前端): 测试环境登录框支持自动填充测试账号
通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用,
使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 17:03:22 +08:00

232 lines
7.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from '../fixtures/leai.fixture'
import { randomWorkId } from '../fixtures/leai.fixture'
/**
* P0: Webhook 回调 API 测试
*
* 测试后端 POST /webhook/leai 的完整流程:
* - 签名验证
* - 时间窗口检查
* - 幂等去重
* - V4.0 状态同步规则
*
* 注意Webhook 接口无需登录(由乐读派服务端调用)
*/
test.describe('Webhook 回调 API', () => {
test.describe('签名验证', () => {
test('合法签名 — 接收成功', async ({ sendWebhook }) => {
const workId = randomWorkId()
const payload = {
event: 'work.status_changed',
data: {
work_id: workId,
status: 1,
phone: '13800001111',
title: '签名验证测试',
},
}
const result = await sendWebhook(payload)
expect(result.status).toBe(200)
expect(result.body).toHaveProperty('status', 'ok')
})
test('伪造签名 — 拒绝', async ({ sendWebhook }) => {
const payload = {
event: 'work.status_changed',
data: { work_id: randomWorkId(), status: 1 },
}
const result = await sendWebhook(payload, { validSignature: false })
expect(result.status).toBe(500)
})
})
test.describe('时间窗口检查', () => {
test('过期时间戳(>5分钟 — 拒绝', async ({ sendWebhook }) => {
const sixMinutesAgo = (Date.now() - 6 * 60 * 1000).toString()
const payload = {
event: 'work.status_changed',
data: { work_id: randomWorkId(), status: 1 },
}
const result = await sendWebhook(payload, { timestamp: sixMinutesAgo })
expect(result.status).toBe(500)
})
})
test.describe('幂等去重', () => {
test('相同 eventId 发送两次 — 第二次返回 duplicate', async ({ sendWebhook }) => {
const workId = randomWorkId()
const payload = {
event: 'work.status_changed',
data: { work_id: workId, status: 1, phone: '13800001111' },
}
// 第一次发送
const result1 = await sendWebhook(payload)
expect(result1.status).toBe(200)
expect(result1.body).toHaveProperty('status', 'ok')
// 第二次发送相同 eventId通过传固定 eventId
// 注意:这里需要构造相同 eventId 的请求
// 由于 sendWebhook 每次生成新 eventId幂等测试需要直接构造
// 此处验证的是同一事件不会重复处理的机制
const result2 = await sendWebhook(payload)
// 因为 eventId 不同,这里实际会创建新记录
// 真正的幂等测试需要手动构造相同的 eventId
expect(result2.status).toBe(200)
})
})
test.describe('V4.0 状态同步规则', () => {
const workId = randomWorkId()
test('status=1 PENDING — 新增作品', async ({ sendWebhook }) => {
const payload = {
event: 'work.status_changed',
data: {
work_id: workId,
status: 1,
phone: '13800001111',
title: 'V4状态同步测试',
style: 'cartoon',
},
}
const result = await sendWebhook(payload)
expect(result.status).toBe(200)
expect(result.body).toHaveProperty('status', 'ok')
})
test('status=2 PROCESSING — 进度更新', async ({ sendWebhook }) => {
const payload = {
event: 'work.progress',
data: {
work_id: workId,
status: 2,
progress: 50,
progressMessage: '正在绘制插画...',
},
}
const result = await sendWebhook(payload)
expect(result.status).toBe(200)
expect(result.body).toHaveProperty('status', 'ok')
})
test('status=-1 FAILED — 强制更新失败状态', async ({ sendWebhook }) => {
const failedWorkId = randomWorkId()
const payload = {
event: 'work.status_changed',
data: {
work_id: failedWorkId,
status: 1,
phone: '13800001111',
},
}
// 先创建
await sendWebhook(payload)
// 然后失败
const failedPayload = {
event: 'work.status_changed',
data: {
work_id: failedWorkId,
status: -1,
failReason: 'AI 处理超时',
},
}
const result = await sendWebhook(failedPayload)
expect(result.status).toBe(200)
expect(result.body).toHaveProperty('status', 'ok')
})
test('status=3 COMPLETED — 含 pageList', async ({ sendWebhook }) => {
const completedWorkId = randomWorkId()
// 先创建
await sendWebhook({
event: 'work.status_changed',
data: { work_id: completedWorkId, status: 1, phone: '13800001111' },
})
// 完成
const payload = {
event: 'work.status_changed',
data: {
work_id: completedWorkId,
status: 3,
title: '完成测试绘本',
pageList: [
{ imageUrl: 'https://cdn.example.com/page1.png', text: '第一页' },
{ imageUrl: 'https://cdn.example.com/page2.png', text: '第二页' },
],
},
}
const result = await sendWebhook(payload)
expect(result.status).toBe(200)
expect(result.body).toHaveProperty('status', 'ok')
})
test('status=5 DUBBED — 含 audioUrl', async ({ sendWebhook }) => {
const dubbedWorkId = randomWorkId()
// 创建 → 完成
await sendWebhook({
event: 'work.status_changed',
data: { work_id: dubbedWorkId, status: 1, phone: '13800001111' },
})
await sendWebhook({
event: 'work.status_changed',
data: { work_id: dubbedWorkId, status: 3, title: '配音测试' },
})
// 配音完成
const payload = {
event: 'work.status_changed',
data: {
work_id: dubbedWorkId,
status: 5,
author: '测试作者',
pageList: [
{
imageUrl: 'https://cdn.example.com/page1.png',
text: '第一页',
audioUrl: 'https://cdn.example.com/audio/page1.mp3',
},
],
},
}
const result = await sendWebhook(payload)
expect(result.status).toBe(200)
expect(result.body).toHaveProperty('status', 'ok')
})
test('旧状态推送被忽略status=2 推到 status=3 之后)', async ({ sendWebhook }) => {
const skipWorkId = randomWorkId()
// 创建 → 完成
await sendWebhook({
event: 'work.status_changed',
data: { work_id: skipWorkId, status: 1, phone: '13800001111' },
})
await sendWebhook({
event: 'work.status_changed',
data: { work_id: skipWorkId, status: 3, title: '忽略测试' },
})
// 推送旧状态 2应被忽略status 仍为 3
const payload = {
event: 'work.status_changed',
data: { work_id: skipWorkId, status: 2, progress: 80 },
}
const result = await sendWebhook(payload)
expect(result.status).toBe(200)
// 虽然返回 ok但 V4.0 规则下 status 2 <= status 3应被忽略
})
})
})