// 导入报告 HTML 渲染测试(Plan 3 Task 8——Task 6 推迟项兑现):钉 Plan 2 Task 6 模板
// 纪律的导入侧镜像——①恶意 payload 全转义(issue/error/键名等所有动态文本通道,
// 报告不得成为存储型 XSS 载体)②结构(分组视图按固定序渲染、封顶注记、错误块)
// ③对象层/渲染层分离(renderImportReportHtml 不改输入——对象层保留原文)。
import { describe, expect, it } from 'vitest'
import type { ImportReport } from '../../apps/api/src/modules/import/import.service.js'
import { renderImportReportHtml } from '../../apps/api/src/modules/import/report-html.js'
const MALICIOUS = ''
const MALICIOUS_ESCAPED = '<script src="https://evil.example/x.js"></script>'
const ATTR_ESCAPE = `">
`
const ATTR_ESCAPE_ESCAPED = '"><img onerror=alert(1) src=x>'
function baseReport(overrides: Partial = {}): ImportReport {
return {
jobId: 'job-0001',
generatedAt: '2026-10-21T00:00:00.000Z',
status: 'succeeded_with_issues',
siteKey: 'demo',
siteId: '11111111-1111-1111-1111-111111111111',
connectionId: '22222222-2222-2222-2222-222222222222',
idempotencyKey: 'import:demo:snap:v1/v1/v1',
attempt: 1,
snapshots: {
wxr: { id: '33333333-3333-3333-3333-333333333333', kind: 'wxr', sha256: 'a'.repeat(64), byteSize: 1024, storagePath: 'incoming/a.xml' },
media_bundle: null,
manual_url_list: null,
},
versions: { adapter: 'a1', mapping: 'm1', sanitizer: 's1', parser: 'p1' },
paths: { productsPath: '/products/', newsPath: '/news/' },
counts: { contentEntries: 3, termsProcessed: 2 },
issues: [],
issuesTruncated: 0,
error: null,
durationMs: 1234.5,
...overrides,
}
}
describe('renderImportReportHtml(Task 8:转义/结构/封顶注记)', () => {
it('所有动态文本通道全转义:issue/错误 summary/快照 id 与 sha——恶意 payload 不以原文出现', () => {
const report = baseReport({
issues: [MALICIOUS, ATTR_ESCAPE],
issueGroups: { content: [MALICIOUS], adapter: [ATTR_ESCAPE] },
error: { code: 'PIPELINE_FAILED', summary: `[pipeline] Error: ${MALICIOUS}` },
})
const html = renderImportReportHtml(report)
// 转义形出现在分组视图与错误块(<>&"' 全集)
expect(html).toContain(MALICIOUS_ESCAPED)
expect(html).toContain(ATTR_ESCAPE_ESCAPED)
expect(html).toContain(`[pipeline] Error: ${MALICIOUS_ESCAPED}`)
// 原文 payload 零出现(骨架无 script 标签——出现即 XSS 面)
expect(html).not.toContain('