fix templates v2

This commit is contained in:
tamakyi
2026-10-22 17:49:25 +08:00
parent bd02eb7ed2
commit b939d26ff0
11 changed files with 436 additions and 60 deletions
+125 -19
View File
@@ -7,9 +7,12 @@ package conf
import (
"testing"
"time"
"github.com/tamakyi/TamaBox/internal/i18n"
)
// setAnnouncement 在测试期间覆盖全局公告配置,结束后恢复。
// 不设 Timezone,即走默认的东八区。
func setAnnouncement(t *testing.T, content, start, end string) {
t.Helper()
old := Announcement
@@ -18,9 +21,22 @@ func setAnnouncement(t *testing.T, content, start, end string) {
sanitizeAnnouncement()
}
func mustParse(t *testing.T, layout, value string) time.Time {
// setAnnouncementConfig 用完整配置覆盖,供分语言 / 时区用例使用。
func setAnnouncementConfig(t *testing.T, cfg announcementConfig) {
t.Helper()
parsed, err := time.ParseInLocation(layout, value, time.Local)
old := Announcement
t.Cleanup(func() { Announcement = old })
Announcement = cfg
sanitizeAnnouncement()
}
func mustParseIn(t *testing.T, layout, value string) time.Time {
t.Helper()
loc, err := time.LoadLocation(defaultAnnouncementZone)
if err != nil {
t.Fatalf("load zone %s: %v", defaultAnnouncementZone, err)
}
parsed, err := time.ParseInLocation(layout, value, loc)
if err != nil {
t.Fatalf("parse %q: %v", value, err)
}
@@ -30,16 +46,16 @@ func mustParse(t *testing.T, layout, value string) time.Time {
func TestAnnouncementDisabledWhenContentEmpty(t *testing.T) {
setAnnouncement(t, " ", "2026-10-24 00:00", "2026-10-26 00:00")
if _, ok := Announcement.Active(time.Now()); ok {
t.Fatal("content 为空时公告应关闭")
if _, ok := Announcement.Active(time.Now(), i18n.LangZhCN); ok {
t.Fatal("所有语言文案都为空时公告应关闭")
}
}
func TestAnnouncementWindowIsHalfOpen(t *testing.T) {
setAnnouncement(t, "维护通知", "2026-10-24 00:00", "2026-10-26 00:00")
start := mustParse(t, "2006-01-02 15:04", "2026-10-24 00:00")
end := mustParse(t, "2006-01-02 15:04", "2026-10-26 00:00")
start := mustParseIn(t, "2006-01-02 15:04", "2026-10-24 00:00")
end := mustParseIn(t, "2006-01-02 15:04", "2026-10-26 00:00")
cases := []struct {
name string
@@ -54,7 +70,7 @@ func TestAnnouncementWindowIsHalfOpen(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
content, ok := Announcement.Active(c.now)
content, ok := Announcement.Active(c.now, i18n.LangZhCN)
if ok != c.want {
t.Fatalf("Active(%s) = %v, want %v", c.now, ok, c.want)
}
@@ -67,33 +83,71 @@ func TestAnnouncementWindowIsHalfOpen(t *testing.T) {
// 只写一侧的边界必须保持「该侧不限」,不能因为另一侧为空就整体失效。
func TestAnnouncementOneSidedWindow(t *testing.T) {
now := time.Date(2026, 10, 22, 12, 0, 0, 0, time.Local)
now := mustParseIn(t, "2006-01-02 15:04", "2026-10-22 12:00")
setAnnouncement(t, "只有开始", "2026-10-23 00:00", "")
if _, ok := Announcement.Active(now); ok {
if _, ok := Announcement.Active(now, i18n.LangZhCN); ok {
t.Fatal("只写 start 且尚未到点时应不展示")
}
if _, ok := Announcement.Active(now.Add(48 * time.Hour)); !ok {
if _, ok := Announcement.Active(now.Add(48*time.Hour), i18n.LangZhCN); !ok {
t.Fatal("只写 start 时到点后应一直展示")
}
setAnnouncement(t, "只有结束", "", "2026-10-23 00:00")
if _, ok := Announcement.Active(now); !ok {
if _, ok := Announcement.Active(now, i18n.LangZhCN); !ok {
t.Fatal("只写 end 且未到点时应立即展示")
}
if _, ok := Announcement.Active(now.Add(48 * time.Hour)); ok {
if _, ok := Announcement.Active(now.Add(48*time.Hour), i18n.LangZhCN); ok {
t.Fatal("只写 end 时过点后应下线")
}
setAnnouncement(t, "一直展示", "", "")
if _, ok := Announcement.Active(now.AddDate(5, 0, 0)); !ok {
if _, ok := Announcement.Active(now.AddDate(5, 0, 0), i18n.LangZhCN); !ok {
t.Fatal("起止都留空时应一直展示")
}
}
// 分语言文案:配了就用自己的,没配回退 content;content 也没配则该语言不显示。
func TestAnnouncementLanguageSelection(t *testing.T) {
setAnnouncementConfig(t, announcementConfig{
Content: "fallback",
ContentZhCN: "简中",
ContentEn: "english",
ContentJa: "日本語",
})
cases := []struct{ lang, want string }{
{i18n.LangZhCN, "简中"},
{i18n.LangEn, "english"},
{i18n.LangJa, "日本語"},
{i18n.LangZhTW, "fallback"}, // 没配繁中 → 回退默认文案
{"en-US", "english"}, // Resolve 会归一化
}
for _, c := range cases {
got, ok := Announcement.Active(time.Now(), c.lang)
if !ok || got != c.want {
t.Fatalf("Active(lang=%s) = %q ok=%v, want %q", c.lang, got, ok, c.want)
}
}
// 只配英文、content 留空:简中访客不应看到英文公告。
setAnnouncementConfig(t, announcementConfig{ContentEn: "english only"})
if _, ok := Announcement.Active(time.Now(), i18n.LangZhCN); ok {
t.Fatal("该语言未配文案且无默认文案时不应展示")
}
if got, ok := Announcement.Active(time.Now(), i18n.LangEn); !ok || got != "english only" {
t.Fatalf("英文应正常展示,got %q ok=%v", got, ok)
}
}
func TestAnnouncementTimeFormats(t *testing.T) {
// 带偏移的 RFC3339 必须按字面时刻解析,而不是再套一层本地时区。
at, ok := parseAnnouncementTime("2026-10-25T02:00:00+08:00")
loc, err := time.LoadLocation(defaultAnnouncementZone)
if err != nil {
t.Fatalf("load zone: %v", err)
}
// 带偏移的 RFC3339 必须按字面时刻解析,而不是再套一层默认时区。
at, ok := parseAnnouncementTime("2026-10-25T02:00:00+08:00", loc)
if !ok {
t.Fatal("RFC3339 应可解析")
}
@@ -102,20 +156,72 @@ func TestAnnouncementTimeFormats(t *testing.T) {
t.Fatalf("解析结果 = %s, want %s", at.UTC(), want)
}
if at, ok := parseAnnouncementTime("2026-10-25"); !ok || at.Hour() != 0 {
if at, ok := parseAnnouncementTime("2026-10-25", loc); !ok || at.Hour() != 0 {
t.Fatalf("纯日期应解析为当天 00:00,got %s ok=%v", at, ok)
}
if at, ok := parseAnnouncementTime("2026-10-25 02:00:59"); !ok || at.Second() != 59 {
if at, ok := parseAnnouncementTime("2026-10-25 02:00:59", loc); !ok || at.Second() != 59 {
t.Fatalf("秒级格式应可解析,got %s ok=%v", at, ok)
}
if _, ok := parseAnnouncementTime("2026/10/25 02:00"); ok {
if _, ok := parseAnnouncementTime("2026/10/25 02:00", loc); ok {
t.Fatal("错误格式应解析失败")
}
if _, ok := parseAnnouncementTime("2026-13-01"); ok {
if _, ok := parseAnnouncementTime("2026-13-01", loc); ok {
t.Fatal("非法月份应解析失败")
}
}
// 裸时间必须按东八区解释:运维写「10-25 02:00」指北京时间,
// 而容器里的 time.Local 是 UTC(旧实现整体偏 8 小时)。
func TestAnnouncementBareTimeUsesBeijing(t *testing.T) {
setAnnouncement(t, "维护通知", "2026-10-25 02:00", "")
want := time.Date(2026, 10, 24, 18, 0, 0, 0, time.UTC) // 北京 10-25 02:00
if !Announcement.StartAt.Equal(want) {
t.Fatalf("裸时间应按 +08:00 解释,got %s, want %s", Announcement.StartAt.UTC(), want)
}
if _, offset := Announcement.StartAt.Zone(); offset != 8*60*60 {
t.Fatalf("解析后的偏移 = %d 秒, want 28800", offset)
}
}
// timezone 显式指定时以它为准,不能继续套东八区。
func TestAnnouncementTimezoneOverride(t *testing.T) {
setAnnouncementConfig(t, announcementConfig{
Content: "维护通知",
Start: "2026-10-25 00:00",
Timezone: "UTC",
})
want := time.Date(2026, 10, 25, 0, 0, 0, 0, time.UTC)
if !Announcement.StartAt.Equal(want) {
t.Fatalf("timezone=UTC 时应按 UTC 解析,got %s", Announcement.StartAt.UTC())
}
setAnnouncementConfig(t, announcementConfig{
Content: "维护通知",
Start: "2026-10-25 00:00",
Timezone: "America/New_York",
})
// 纽约 10-25 00:00 = UTC 10-25 04:00(EDT,UTC-4)
wantNY := time.Date(2026, 10, 25, 4, 0, 0, 0, time.UTC)
if !Announcement.StartAt.Equal(wantNY) {
t.Fatalf("timezone=America/New_York 解析错误,got %s", Announcement.StartAt.UTC())
}
}
// 时区名写错和日期写错一样:只关公告,不阻断启动。
func TestAnnouncementInvalidTimezoneDisables(t *testing.T) {
setAnnouncementConfig(t, announcementConfig{
Content: "维护通知",
Start: "2026-10-25 00:00",
Timezone: "Mars/Olympus",
})
if Announcement.Content != "" {
t.Fatal("时区无效时公告应被关闭")
}
}
// 时间写错只关公告,不能让站点起不来;同时必须清空解析结果,避免残留时间窗。
func TestAnnouncementInvalidTimeDisables(t *testing.T) {
for _, c := range []struct{ name, start, end string }{