Files
tamabox/internal/template/markdown_test.go
T
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00

172 lines
5.8 KiB
Go
Raw 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.
package template
import (
"strings"
"testing"
)
func TestMarkdownToHTML_Basic(t *testing.T) {
cases := []struct {
name string
in string
want string // 必须包含
}{
{"bold", "**bold**", "<strong>bold</strong>"},
{"italic", "*italic*", "<em>italic</em>"},
{"link", "[baidu](https://baidu.com)", `href="https://baidu.com"`},
{"list", "- item1\n- item2", "<li>item1</li>"},
{"code", "`code`", "<code>code</code>"},
{"newline", "line1\nline2", "<br>"},
{"font-red", `<font color="red">文本</font>`, `<font color="red">文本</font>`},
{"font-hex", `<font color="#ff0000">红</font>`, `<font color="#ff0000">红</font>`},
{"font-rgb", `<font color="rgb(255,0,0)">红</font>`, `<font color="rgb(255,0,0)">红</font>`},
{"font-mixed", `**粗体** 和 <font color="blue">蓝</font>`, `<font color="blue">蓝</font>`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := string(MarkdownToHTML(tc.in))
if !strings.Contains(got, tc.want) {
t.Errorf("MarkdownToHTML(%q) = %q, 期望包含 %q", tc.in, got, tc.want)
}
})
}
}
func TestMarkdownToHTML_XSS(t *testing.T) {
cases := []string{
`&lt;script&gt;alert(1)&lt;/script&gt;`,
`&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;`,
`<script>alert(1)</script>`,
`<img src=x onerror=alert(1)>`,
`[x](javascript:alert(1))`,
`<a href="javascript:alert(1)">x</a>`,
`<iframe src="https://evil.com"></iframe>`,
"<svg onload=alert(1)>",
`<div onclick="alert(1)">x</div>`,
`<style>body{display:none}</style>`,
}
for _, in := range cases {
got := string(MarkdownToHTML(in))
if strings.Contains(got, "<script") || strings.Contains(got, " onerror") ||
strings.Contains(got, " onclick") || strings.Contains(got, " onload") ||
strings.Contains(got, "<iframe") || strings.Contains(got, "javascript:") ||
strings.Contains(got, "<style") || strings.Contains(got, "<svg") {
t.Errorf("XSS 防护失败: 输入 %q 输出 %q", in, got)
}
}
}
// TestMarkdownToHTML_FontColorInjection font 的 color 属性只放行合法颜色值,
// 注入尝试必须被丢弃(标签保留但 color 属性被剥离,且不产生新属性)。
func TestMarkdownToHTML_FontColorInjection(t *testing.T) {
cases := []struct {
name string
in string
want string // 输出必须包含(剥离后仅剩文本)
}{
{"css-inject", `<font color="red;position:fixed">x</font>`, "x"},
{"quote-inject", `<font color="red" onclick="alert(1)">x</font>`, "x"},
{"url-inject", `<font color="url(javascript:alert(1))">x</font>`, "x"},
{"onattr-inject", `<font color="x" style="background:url(javascript:1)">x</font>`, "x"},
{"long-value", `<font color="` + strings.Repeat("a", 50) + `">x</font>`, "x"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := string(MarkdownToHTML(tc.in))
if !strings.Contains(got, tc.want) {
t.Errorf("输出 %q 应包含 %q", got, tc.want)
}
// 注入的 color 值不得出现
for _, evil := range []string{";", "position:", "onclick", "javascript:", "<font color=\"red\" onclick", "<font color=\"x\" style"} {
if strings.Contains(got, evil) {
t.Errorf("注入残留: %q 包含 %q", got, evil)
}
}
})
}
}
func TestMarkdownToHTML_Empty(t *testing.T) {
if got := string(MarkdownToHTML("")); got != "" {
t.Errorf("空输入应输出空串, got %q", got)
}
}
func TestMarkdownToPlainText(t *testing.T) {
cases := []struct {
name string
in string
want string // 必须包含(忽略换行折叠)
}{
{"plain", "你好世界", "你好世界"},
{"bold", "**粗体**", "粗体"},
{"font", `<font color="red">红字</font>`, "红字"},
{"link", "[百度](https://baidu.com)", "百度"},
{"code", "`code`", "code"},
{"list", "- 第一项\n- 第二项", "第一项"},
{"block", "# 标题\n内容", "标题"},
{"mixed", "问题:**重要** 详情 <font color=\"blue\">蓝</font>", "重要"},
{"empty", "", ""},
// 危险标签被剥离:script 的内容是纯文本(绘制无风险),img 自闭合无文本则输出空
{"xss-script", "<script>alert(1)</script>", "alert(1)"},
{"xss-img", `<img src=x onerror=alert(1)>`, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := strings.Join(strings.Fields(MarkdownToPlainText(tc.in)), " ")
if tc.want == "" {
if got != "" {
t.Errorf("MarkdownToPlainText(%q) = %q, 期望空", tc.in, got)
}
return
}
if !strings.Contains(got, tc.want) {
t.Errorf("MarkdownToPlainText(%q) = %q, 期望包含 %q", tc.in, got, tc.want)
}
// 输出不得残留任何标签(< 必须被剥离)
if strings.Contains(got, "<") {
t.Errorf("MarkdownToPlainText(%q) = %q, 不应包含标签字符", tc.in, got)
}
})
}
}
// TestSafeURL 是 P2-1 的回归测试:markdown 链接白名单必须拒绝协议相对 URL
// (//evil.com 等),否则会被误判为站内相对路径而渲染成指向外站的钓鱼 / 外链埋点链接。
// 同时验证 javascript:/data:/ftp: 等危险协议与大小写变体均被拒绝,http/https/mailto
// 与站内相对路径被放行。
func TestSafeURL(t *testing.T) {
allow := []string{
"https://example.com",
"http://example.com",
"HTTPS://EXAMPLE.COM", // 大小写不敏感
"mailto:[email protected]",
"/path/to/page",
"./relative",
"../up",
"#fragment",
}
for _, u := range allow {
if !safeURL(u) {
t.Errorf("safeURL(%q) = false, 期望放行", u)
}
}
deny := []string{
"//evil.com", // 协议相对 URL(核心回归点)
`\\evil.com`, // 反斜杠变体
`/\evil.com`, // 反斜杠变体
"javascript:alert(1)", // 危险协议
"JavaScript:alert(1)", // 大小写变体
"data:text/html,<script>",
"ftp://example.com",
"", // 空串
}
for _, u := range deny {
if safeURL(u) {
t.Errorf("safeURL(%q) = true, 期望拒绝", u)
}
}
}