package templates import ( gotemplate "html/template" "strings" "testing" "time" "github.com/tamakyi/TamaBox/internal/conf" ) type mockPageQuestion struct { ID uint Content string CreatedAt time.Time IsPinned bool Answer string } func publicBaseData() map[string]interface{} { pageUser := map[string]interface{}{ "Domain": "tester", "Name": "测试箱主", "Avatar": "", "Background": "", "Intro": "欢迎提问", "Usernamecolor": "#000", "Introcolor": "#666", "Acfunlink": "none", "Bililink": "none", "Weibolink": "none", "Xiaohongshulink": "none", "Bloglink": "none", "Qrcodebackcolor": "#fff", "Qrcodecolor": "#000", "Dotscale": "0.35", "BackgroundImage": "", "Backgroundimagealpha": "0.8", "Qrcodepdpcolor": "#000", "HarassmentSetting": "disabled", "AllowChat": true, "ChatMaxMessages": 10, } return map[string]interface{}{ "LangCode": "zh-CN", "SupportedLangs": []map[string]string{{"Code": "zh-CN", "Name": "简体中文"}}, "IsLogged": false, "IsUserPage": true, "IsOwnPage": false, "IsPixel": false, "VConsole": false, "CSRFTokenHTML": gotemplate.HTML(``), "RecaptchaSiteKey": "", "CaptchaType": "recaptcha", "PageUser": pageUser, "PageUserRaw": pageUser, "PageQuestions": []mockPageQuestion{ {ID: 1, Content: "置顶的问题", CreatedAt: time.Now(), IsPinned: true, Answer: "答1"}, {ID: 2, Content: "普通问题", CreatedAt: time.Now().Add(-time.Hour), IsPinned: false, Answer: "答2"}, }, "PageQuestionCursor": "2", "CanAsk": true, "AnsweredCount": 2, "ICP": "", // [limits] 全站上限(生产由 Contexter 全局注入 conf.Limits),供提问/回答/追问 // 输入框 maxlength 与私聊发起框同源 "Limits": conf.Limits, } } // TestRenderPublicQuestionList 渲染提问箱主页(含置顶条目),复现公开页 500。 func TestRenderPublicQuestionList(t *testing.T) { if err := renderWithData(t, "question/list", publicBaseData()); err != nil { t.Fatalf("render question/list failed: %v", err) } } // TestRenderQuestionListClosed 提问功能关闭时:展示关闭原因 + 下次开放/上次关闭时间(问题 3 的模板路径)。 func TestRenderQuestionListClosed(t *testing.T) { base := publicBaseData() base["CanAsk"] = false base["AskState"] = map[string]interface{}{"Open": false, "Reason": "not_started"} base["AskClosedMessage"] = "提问功能尚未开放" base["AskNextOpenAtStr"] = "2026-09-02 10:00" base["AskLastCloseAtStr"] = "2026-09-01 18:00" out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list (closed) failed: %v", err) } if !strings.Contains(out, "提问功能尚未开放") { t.Fatal("closed reason not rendered") } if !strings.Contains(out, "下次开放时间:2026-09-02 10:00") { t.Fatal("next open time not rendered") } if !strings.Contains(out, "上次关闭时间:2026-09-01 18:00") { t.Fatal("last close time not rendered") } } // TestRenderQuestionListClosedPermanent 一次性窗口结束后(无循环定时):只展示上次关闭时间,不展示下次开放。 func TestRenderQuestionListClosedPermanent(t *testing.T) { base := publicBaseData() base["CanAsk"] = false base["AskState"] = map[string]interface{}{"Open": false, "Reason": "ended"} base["AskClosedMessage"] = "提问功能已结束收集" base["AskNextOpenAtStr"] = "" base["AskLastCloseAtStr"] = "2026-08-31 18:00" out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list (ended) failed: %v", err) } if strings.Contains(out, "下次开放时间") { t.Fatal("next open time should NOT be rendered when permanently closed") } if !strings.Contains(out, "上次关闭时间:2026-08-31 18:00") { t.Fatal("last close time not rendered") } } // TestRenderQuestionListRegisterOnlyCaption 未开启「仅允许注册用户」时,二维码文案保持原样。 func TestRenderQuestionListRegisterOnlyCaption(t *testing.T) { base := publicBaseData() base["RegisterOnlyAsk"] = false out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list failed: %v", err) } if !strings.Contains(out, "谁都可以以匿名的形式提问") { t.Fatal("anonymous caption should be rendered when not register-only") } if strings.Contains(out, "仅允许注册用户向TA提问") { t.Fatal("register-only caption should NOT be rendered when not register-only") } } // TestRenderQuestionListRegisterOnlyOn 开启「仅允许注册用户」(箱主登录态可提问)时,二维码文案联动(问题 2)。 func TestRenderQuestionListRegisterOnlyOn(t *testing.T) { base := publicBaseData() base["CanAsk"] = true base["RegisterOnlyAsk"] = true out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list failed: %v", err) } if !strings.Contains(out, "仅允许注册用户向TA提问") { t.Fatal("register-only caption should be rendered when register-only enabled") } if strings.Contains(out, "谁都可以以匿名的形式提问") { t.Fatal("anonymous caption should NOT be rendered when register-only enabled") } } // TestRenderQuestionListVoteDisabled 投票功能关闭时:投票 Tab 保留显示但置灰禁用,并带原因 tooltip。 func TestRenderQuestionListVoteDisabled(t *testing.T) { base := publicBaseData() base["VoteEnabled"] = false base["VoteClosedMessage"] = "该提问箱已关闭投票功能" out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list (vote disabled) failed: %v", err) } if !strings.Contains(out, "profile-tab is-disabled") { t.Fatal("vote tab should stay visible but disabled") } if !strings.Contains(out, "disabled") { t.Fatal("vote tab should carry disabled attribute") } if !strings.Contains(out, "该提问箱已关闭投票功能") { t.Fatal("vote tab should carry the close reason tooltip") } // 注意:JS 守卫代码里也有 [data-profile-tab="votes"] 选择器,须按按钮元素精确断言 if strings.Contains(out, `data-profile-tab="votes" role="tab"`) { t.Fatal("disabled vote tab must NOT be switchable") } } // TestRenderQuestionListVoteEnabled 投票功能正常时:投票 Tab 可点击切换。 func TestRenderQuestionListVoteEnabled(t *testing.T) { base := publicBaseData() base["VoteEnabled"] = true out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list (vote enabled) failed: %v", err) } if !strings.Contains(out, `data-profile-tab="votes"`) { t.Fatal("vote tab should be switchable when enabled") } if strings.Contains(out, "profile-tab is-disabled") { t.Fatal("vote tab must not be disabled when enabled") } } // TestRenderQuestionListChatOnly 提问功能关闭但私聊开启时:保留独立私聊入口(含已登录/未登录两种形态)。 func TestRenderQuestionListChatOnly(t *testing.T) { base := publicBaseData() base["CanAsk"] = false base["AskState"] = map[string]interface{}{"Open": false, "Reason": "disabled"} base["AskClosedMessage"] = "提问箱主人已关闭提问功能" base["AskNextOpenAtStr"] = "" base["AskLastCloseAtStr"] = "" base["AllowChat"] = true base["ChatMaxMessages"] = 20 base["LoggedUser"] = map[string]interface{}{"Domain": "tester", "Name": "测试箱主"} // 已登录:渲染独立私聊表单 out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list (chat only, logged in) failed: %v", err) } if !strings.Contains(out, "提问箱主人已关闭提问功能") { t.Fatal("ask closed reason should still be shown") } if !strings.Contains(out, `id="chat-only-form"`) { t.Fatal("chat-only form should be rendered when chat enabled and logged in") } if !strings.Contains(out, `id="chat-only-form" enctype="multipart/form-data"`) { t.Fatal("chat-only form must be multipart, otherwise form.Bind panics on the Images field") } if !strings.Contains(out, `name="start_chat_val" value="true"`) { t.Fatal("chat-only form must submit as a chat request") } if !strings.Contains(out, "在此处撰写你的私聊消息...") { t.Fatal("chat placeholder should be rendered") } if !strings.Contains(out, "onChatOnlySubmit") { t.Fatal("chat-only captcha callback should be defined for non-math captcha") } // 未登录:渲染登录引导而非表单 base["LoggedUser"] = nil out, err = renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list (chat only, anonymous) failed: %v", err) } if strings.Contains(out, `id="chat-only-form"`) { t.Fatal("chat-only form must not be rendered for anonymous visitors") } if !strings.Contains(out, "/login?to=") { t.Fatal("anonymous visitors should get a login link to start chats") } } // TestRenderQuestionListChatOnlyNoChat 提问关闭且私聊也关闭:不渲染私聊入口。 func TestRenderQuestionListChatOnlyNoChat(t *testing.T) { base := publicBaseData() base["CanAsk"] = false base["AskClosedMessage"] = "提问箱主人已关闭提问功能" base["AllowChat"] = false base["LoggedUser"] = map[string]interface{}{"Domain": "tester", "Name": "测试箱主"} out, err := renderToString(t, "question/list", base) if err != nil { t.Fatalf("render question/list (all closed) failed: %v", err) } if strings.Contains(out, `id="chat-only-form"`) { t.Fatal("chat-only form must not be rendered when chat is disabled") } } // TestRenderPublicQuestionItem 渲染单题页(含历史列表),复现公开页 500。 func TestRenderPublicQuestionItem(t *testing.T) { data := publicBaseData() data["Question"] = map[string]interface{}{ "ID": 1, "Content": "置顶的问题", "Answer": "答1", "CreatedAt": time.Now(), "UpdatedAt": time.Now(), "IsPrivate": false, "IsPinned": true, "Token": "abc123", "ReceiveReplyEmail": "", } data["QuestionToken"] = "abc123" data["AskUploadImages"] = []interface{}{} data["AnswerUploadImages"] = []interface{}{} data["FollowUps"] = []interface{}{} data["CanFollowUp"] = false data["FollowUpRemaining"] = 0 data["FollowUpLimit"] = 3 data["IsOwnPage"] = true data["IsQuestioner"] = false data["IsBlocked"] = false data["AllowChat"] = true if err := renderWithData(t, "question/item", data); err != nil { t.Fatalf("render question/item failed: %v", err) } }