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.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+465
View File
@@ -0,0 +1,465 @@
package templates
import (
gotemplate "html/template"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/flamego/template"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/dbutil"
)
// voteBaseData 构造投票模板渲染的基础数据(仿 render_test.go 的 base)。
func voteBaseData() map[string]interface{} {
return map[string]interface{}{
"Title": "投票",
"LangCode": "zh-CN",
"SupportedLangs": []map[string]string{{"Code": "zh-CN", "Name": "简体中文"}},
"IsLogged": true,
"LoggedUser": map[string]interface{}{"Domain": "test", "Name": "tester"},
"LoggedUserName": "tester",
"CurrentURI": "/vote/abc123",
"IsUserPage": false,
"IsPixel": false,
"VConsole": false,
"CSRFTokenHTML": gotemplate.HTML(`<input type="hidden" name="_csrf" value="x">`),
"RecaptchaSiteKey": "",
"CaptchaType": "recaptcha",
"PageUser": map[string]interface{}{"Domain": "test", "Name": "tester", "Avatar": ""},
"ICP": "",
// 参与端自定义补充选项的字数上限(生产由 route/vote 注入 conf.Limits.CustomAnswerLen)
"MaxCustomTextLen": conf.Limits.CustomAnswerLen,
"CustomFieldMarker": db.CustomFieldMarker,
// 站长侧单个选项文本上限(生产由 route/user 注入 conf.Limits.VoteOptionLen)
"MaxVoteOptionLen": conf.Limits.VoteOptionLen,
// [limits] 全站上限(生产由 Contexter 全局注入 conf.Limits),
// 供 question/list 嵌套的提问框 maxlength 同源
"Limits": conf.Limits,
}
}
func TestVoteFormUsesFixedDeviceLimitNotice(t *testing.T) {
fs, err := template.EmbedFS(FS, ".", []string{".html"})
if err != nil {
t.Fatalf("EmbedFS: %v", err)
}
for _, f := range fs.Files() {
if f.Name() != "user/vote-form" && f.Name() != "user/vote-form.html" {
continue
}
data, err := f.Data()
if err != nil {
t.Fatalf("get template data: %v", err)
}
source := string(data)
if strings.Contains(source, `name="max_votes_per_fingerprint"`) {
t.Fatal("fixed fingerprint limit must not be rendered as a form input")
}
if !strings.Contains(source, `vote.formMaxVotesPerFingerprint`) {
t.Fatal("fixed fingerprint limit notice is missing")
}
return
}
t.Fatal("user/vote-form.html not found")
}
func TestRenderVotePage(t *testing.T) {
opt1 := &db.VoteOption{Content: "选项 A"}
opt2 := &db.VoteOption{Content: "选项 B"}
t.Run("single can vote", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "测试单选", Description: "描述", Type: db.VoteTypeSingle, LinkToken: "abc123", MaxChoices: 1}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["Options"] = []*db.VoteOption{opt1, opt2}
base["HasVoted"] = false
base["CanVote"] = true
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = false
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (single) failed: %v", err)
}
})
t.Run("multiple can vote", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "多选", Type: db.VoteTypeMultiple, LinkToken: "abc123", MaxChoices: 2}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["Options"] = []*db.VoteOption{opt1, opt2, {Content: "选项 C"}}
base["HasVoted"] = false
base["CanVote"] = true
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = false
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (multiple) failed: %v", err)
}
})
t.Run("rating can vote", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "评分", Type: db.VoteTypeRating, LinkToken: "abc123"}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["HasVoted"] = false
base["CanVote"] = true
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = false
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (rating) failed: %v", err)
}
})
t.Run("rank can vote", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "排序", Type: db.VoteTypeRank, LinkToken: "abc123"}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["Options"] = []*db.VoteOption{opt1, opt2}
base["HasVoted"] = false
base["CanVote"] = true
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = false
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (rank) failed: %v", err)
}
})
t.Run("preview draft form", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "草稿表单", Type: db.VoteTypeForm, LinkToken: "abc123"}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["FormFields"] = []*db.VoteFormField{
{Model: dbutil.Model{ID: 1}, FieldType: db.VoteFormFieldTypeText, Label: "昵称", Required: true, MaxLength: 20},
{Model: dbutil.Model{ID: 2}, FieldType: db.VoteFormFieldTypeRadio, Label: "满意度", Options: `["满意","一般"]`},
}
base["HasVoted"] = false
base["CanVote"] = true
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = true
base["IsLogged"] = true
base["PreviewMode"] = true
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (preview) failed: %v", err)
}
})
t.Run("custom text limit injected", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "自定义选项", Type: db.VoteTypeSingle, LinkToken: "abc123", AllowCustom: true}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["FormFields"] = []*db.VoteFormField{
{Model: dbutil.Model{ID: 1}, FieldType: db.VoteFormFieldTypeRadio, Label: "满意度", Options: `["满意","一般"]`, AllowCustom: true},
}
base["HasVoted"] = false
base["CanVote"] = true
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = false
out, err := renderToString(t, "vote/page", base)
if err != nil {
t.Fatalf("render vote/page (custom) failed: %v", err)
}
// 缺注入时模板不报错,只会渲染出 <no value> 让整段脚本失效,必须显式断言
if strings.Contains(out, "<no value>") {
t.Fatal("rendered page contains <no value>: MaxCustomTextLen was not injected")
}
// 注意:html/template 在 <script> 上下文会主动插入安全空格,
// 实际输出形如 `var MAX_CUSTOM_LEN = 200 ;`,因此用正则而非字面量匹配。
want := regexp.MustCompile(`var MAX_CUSTOM_LEN\s*=\s*` + strconv.Itoa(conf.Limits.CustomAnswerLen) + `\s*;`)
if !want.MatchString(out) {
t.Fatalf("missing custom text limit constant, want /%s/", want)
}
if !strings.Contains(out, "data-tb-text-counter") {
t.Fatal("custom inputs are missing the data-tb-text-counter hook")
}
})
t.Run("form can vote", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "表单", Type: db.VoteTypeForm, LinkToken: "abc123"}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["FormFields"] = []*db.VoteFormField{
{Model: dbutil.Model{ID: 1}, FieldType: db.VoteFormFieldTypeText, Label: "昵称", Required: true, MaxLength: 20},
{Model: dbutil.Model{ID: 2}, FieldType: db.VoteFormFieldTypeRadio, Label: "满意度", Options: `["满意","一般"]`},
{Model: dbutil.Model{ID: 3}, FieldType: db.VoteFormFieldTypeCheckbox, Label: "喜欢的", Options: `["A","B","C"]`},
{Model: dbutil.Model{ID: 4}, FieldType: db.VoteFormFieldTypeRating, Label: "评分"},
{Model: dbutil.Model{ID: 5}, FieldType: db.VoteFormFieldTypeRank, Label: "排序", Options: `["首选","次选","末选"]`},
}
base["HasVoted"] = false
base["CanVote"] = true
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = false
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (form) failed: %v", err)
}
})
t.Run("already voted with results", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "已投", Type: db.VoteTypeSingle, LinkToken: "abc123"}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["Options"] = []*db.VoteOption{opt1, opt2}
base["HasVoted"] = true
base["CanVote"] = false
base["NotStarted"] = false
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = true
base["IsOwner"] = false
base["Results"] = map[string]interface{}{
"Type": "single",
"Total": int64(2),
"Options": []interface{}{},
}
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (voted) failed: %v", err)
}
})
t.Run("not started", func(t *testing.T) {
base := voteBaseData()
base["Vote"] = &db.Vote{Title: "未开始", Type: db.VoteTypeSingle, LinkToken: "abc123"}
base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""}
base["HasVoted"] = false
base["CanVote"] = false
base["NotStarted"] = true
base["Ended"] = false
base["VoteScopeRegisteredOnly"] = false
base["ShowResults"] = false
base["IsOwner"] = false
if err := renderWithData(t, "vote/page", base); err != nil {
t.Fatalf("render vote/page (not started) failed: %v", err)
}
})
}
func TestRenderVoteManagementPages(t *testing.T) {
t.Run("question list votes panel", func(t *testing.T) {
base := voteBaseData()
base["IsUserPage"] = true
// 提问箱页面渲染所需的字段(与 QuestionList 提供的一致)
base["Stats"] = map[string]int64{"total": 0, "answered": 0, "unanswered": 0, "private": 0, "pinned": 0, "chats": 0}
base["Keyword"] = ""
base["SelectedSort"] = "latest"
base["SelectedStatus"] = "all"
base["SelectedType"] = "all"
base["ReceivedItems"] = []interface{}{}
base["ConversationDetails"] = []interface{}{}
// 投票面板数据:投票管理已合并到「我的提问箱 · 投票」面板
base["VoteStats"] = map[string]int64{"total": 4, "published": 2}
base["MyVotes"] = []interface{}{
map[string]interface{}{
"ID": 1, "Title": "投票 1", "Type": "single", "Status": "draft", "EffectiveStatus": "draft",
"SubmissionCount": int64(0), "OptionCount": int64(2),
"CreatedAt": time.Now(), "LinkToken": "abc",
},
map[string]interface{}{
"ID": 2, "Title": "投票 2", "Type": "multiple", "Status": "published", "EffectiveStatus": "ended",
"SubmissionCount": int64(5), "OptionCount": int64(3),
"CreatedAt": time.Now(), "LinkToken": "def",
},
map[string]interface{}{
"ID": 3, "Title": "投票 3", "Type": "single", "Status": "published", "EffectiveStatus": "published",
"SubmissionCount": int64(2), "OptionCount": int64(2),
"CreatedAt": time.Now(), "LinkToken": "ghi", "RemainingText": "剩余 2 天 3 小时",
},
map[string]interface{}{
"ID": 4, "Title": "投票 4", "Type": "form", "Status": "published", "EffectiveStatus": "published",
"SubmissionCount": int64(1), "OptionCount": int64(0),
"CreatedAt": time.Now(), "LinkToken": "jkl", "RemainingText": "无限期",
},
}
if err := renderWithData(t, "user/question-list", base); err != nil {
t.Fatalf("render user/question-list (votes panel) failed: %v", err)
}
})
t.Run("vote form new", func(t *testing.T) {
base := voteBaseData()
base["IsUserPage"] = true
base["IsEdit"] = false
out, err := renderToString(t, "user/vote-form", base)
if err != nil {
t.Fatalf("render user/vote-form (new) failed: %v", err)
}
// 站长侧选项上限常量已注入并应用到两类选项输入(单选/多选选项 + 字段选项)。
// 缺注入时渲染出的会是 var MAX_OPTION_LEN = <no value>;,两个 maxlength 也失去同源上限。
wantVar := regexp.MustCompile(`var MAX_OPTION_LEN\s*=\s*` + strconv.Itoa(conf.Limits.VoteOptionLen) + `\s*;`)
if !wantVar.MatchString(out) {
t.Fatalf("vote-form: 缺 MAX_OPTION_LEN 注入,want %d", conf.Limits.VoteOptionLen)
}
wantMax := `maxlength="' + MAX_OPTION_LEN + '"`
if n := strings.Count(out, wantMax); n != 2 {
t.Fatalf("vote-form: 选项输入框应有两处引用 MAX_OPTION_LEN,实际 %d 处", n)
}
})
t.Run("vote form edit", func(t *testing.T) {
base := voteBaseData()
base["IsUserPage"] = true
base["IsEdit"] = true
base["Vote"] = &db.Vote{Model: dbutil.Model{ID: 1}, Title: "编辑中", Type: db.VoteTypeMultiple, Visibility: "public",
VoterScope: "anyone", ShowResult: "after_vote", MaxChoices: 3, MaxVotesPerFingerprint: 1, RequireCaptcha: true}
base["InitialOptions"] = []string{"A", "B"}
base["InitialFields"] = []interface{}{}
if err := renderWithData(t, "user/vote-form", base); err != nil {
t.Fatalf("render user/vote-form (edit) failed: %v", err)
}
})
t.Run("vote form edit locked", func(t *testing.T) {
base := voteBaseData()
base["IsUserPage"] = true
base["IsEdit"] = true
base["LockedContent"] = true
base["Vote"] = &db.Vote{Model: dbutil.Model{ID: 1}, Title: "进行中", Type: db.VoteTypeSingle, Visibility: "public",
VoterScope: "anyone", ShowResult: "after_vote", MaxChoices: 1, MaxVotesPerFingerprint: 1, RequireCaptcha: true}
base["InitialOptions"] = []string{"A", "B"}
base["InitialFields"] = []interface{}{}
if err := renderWithData(t, "user/vote-form", base); err != nil {
t.Fatalf("render user/vote-form (locked) failed: %v", err)
}
})
t.Run("vote results single", func(t *testing.T) {
base := voteBaseData()
base["IsUserPage"] = true
base["Vote"] = &db.Vote{Model: dbutil.Model{ID: 1}, Title: "结果", Type: db.VoteTypeSingle}
base["Total"] = int64(2)
base["Results"] = map[string]interface{}{
"Type": "single",
"Options": []interface{}{
map[string]interface{}{"Option": map[string]interface{}{"Content": "A"}, "Count": int64(2), "Percent": 100},
},
"CustomStats": []interface{}{
map[string]interface{}{"Text": "自定义项", "Count": int64(1)},
},
}
if err := renderWithData(t, "user/vote-results", base); err != nil {
t.Fatalf("render user/vote-results (single) failed: %v", err)
}
})
t.Run("vote results form", func(t *testing.T) {
base := voteBaseData()
base["IsUserPage"] = true
base["Vote"] = &db.Vote{Model: dbutil.Model{ID: 1}, Title: "表单结果", Type: db.VoteTypeForm}
base["Total"] = int64(1)
base["FormFields"] = []*db.VoteFormField{
{Label: "昵称", FieldType: db.VoteFormFieldTypeText},
{Label: "满意度", FieldType: db.VoteFormFieldTypeRadio, Options: `["满意","一般"]`},
}
base["Results"] = map[string]interface{}{
"Type": "form", "FormCount": int64(1), "FormFieldStats": []interface{}{
map[string]interface{}{
"Field": map[string]interface{}{"Label": "最喜欢的水果", "FieldType": "radio", "Options": `["苹果","香蕉"]`},
"Total": int64(1),
"OptionStats": []interface{}{
map[string]interface{}{"Option": map[string]interface{}{"Content": "苹果"}, "Count": int64(1)},
map[string]interface{}{"Option": map[string]interface{}{"Content": "香蕉"}, "Count": int64(0)},
},
},
map[string]interface{}{
"Field": map[string]interface{}{"Label": "整体评价", "FieldType": "rating"},
"Total": int64(1), "AvgRating": 9.0,
"RatingCounts": []interface{}{},
},
map[string]interface{}{
"Field": map[string]interface{}{"Label": "按喜好排序", "FieldType": "rank"},
"Total": int64(1), "AvgRank": 1.5,
"OptionStats": []interface{}{
map[string]interface{}{"Option": map[string]interface{}{"Content": "A"}, "Count": int64(1), "AvgRank": 1.0},
map[string]interface{}{"Option": map[string]interface{}{"Content": "B"}, "Count": int64(1), "AvgRank": 2.0},
},
},
map[string]interface{}{
"Field": map[string]interface{}{"Label": "建议", "FieldType": "textarea"},
"Total": int64(1), "TextCount": int64(1),
},
},
}
// 文本类字段逐条提交内容按字段标签归类(内联在题目下方折叠展示)
base["TextReplies"] = map[string]interface{}{
"建议": []interface{}{
map[string]interface{}{"CreatedAt": time.Now(), "Text": "希望支持深色模式"},
},
}
if err := renderWithData(t, "user/vote-results", base); err != nil {
t.Fatalf("render user/vote-results (form) failed: %v", err)
}
})
}
func TestRenderQuestionListWithVotes(t *testing.T) {
base := voteBaseData()
base["PageUser"] = map[string]interface{}{"Domain": "test", "Name": "tester", "Avatar": "",
"Background": "", "Intro": "", "UseCustomQRColor": false, "Qrcodebackcolor": "", "Qrcodecolor": "",
"Dotscale": "", "BackgroundImage": "", "Backgroundimagealpha": "", "Qrcodepdpcolor": "",
"Acfunlink": "", "Bililink": "", "Weibolink": "", "Xiaohongshulink": "", "Bloglink": "",
"Usernamecolor": "#000000", "Introcolor": "#000000"}
base["CanAsk"] = true
base["VoteEnabled"] = true
base["PageVotes"] = []*db.Vote{{Title: "板块投票", LinkToken: "abc123"}}
base["PageQuestions"] = []interface{}{}
base["AnsweredCount"] = int64(0)
base["PageQuestionCursor"] = ""
base["AllowChat"] = false
base["ChatMaxMessages"] = 20
if err := renderWithData(t, "question/list", base); err != nil {
t.Fatalf("render question/list with votes failed: %v", err)
}
// 箱主本人访问自己的提问箱:无进行中投票时也应显示管理入口(空态)
base["IsOwnPage"] = true
base["PageVotes"] = []interface{}{}
if err := renderWithData(t, "question/list", base); err != nil {
t.Fatalf("render question/list (own page, empty) failed: %v", err)
}
}
func TestRenderExploreWithHotVotes(t *testing.T) {
base := voteBaseData()
base["Users"] = []interface{}{}
base["RecommendUsers"] = []interface{}{}
base["HotTags"] = []string{}
base["Keyword"] = ""
base["Sort"] = "latest"
base["Page"] = 1
base["TotalPages"] = 1
base["PrevPage"] = 0
base["NextPage"] = 2
base["HotVotes"] = []*db.Vote{
{Title: "热门投票 1", LinkToken: "abc1", Type: db.VoteTypeSingle},
{Title: "热门投票 2", LinkToken: "abc2", Type: db.VoteTypeForm},
}
base["AllVotes"] = []*db.Vote{
{Title: "全部投票 1", LinkToken: "abc3", Type: db.VoteTypeMultiple, Model: dbutil.Model{CreatedAt: time.Now()}},
}
if err := renderWithData(t, "user/explore", base); err != nil {
t.Fatalf("render user/explore with hot votes failed: %v", err)
}
}