// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. 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 t.Cleanup(func() { Announcement = old }) Announcement = announcementConfig{Content: content, Start: start, End: end} sanitizeAnnouncement() } // setAnnouncementConfig 用完整配置覆盖,供分语言 / 时区用例使用。 func setAnnouncementConfig(t *testing.T, cfg announcementConfig) { t.Helper() 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) } return parsed } func TestAnnouncementDisabledWhenContentEmpty(t *testing.T) { setAnnouncement(t, " ", "2026-10-24 00:00", "2026-10-26 00:00") 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 := 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 now time.Time want bool }{ {"开始前一秒", start.Add(-time.Second), false}, {"开始时刻(闭区间)", start, true}, {"窗口内", start.Add(12 * time.Hour), true}, {"结束时刻(开区间)", end, false}, {"结束后", end.Add(time.Hour), false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { content, ok := Announcement.Active(c.now, i18n.LangZhCN) if ok != c.want { t.Fatalf("Active(%s) = %v, want %v", c.now, ok, c.want) } if ok && content != "维护通知" { t.Fatalf("content = %q, want 维护通知", content) } }) } } // 只写一侧的边界必须保持「该侧不限」,不能因为另一侧为空就整体失效。 func TestAnnouncementOneSidedWindow(t *testing.T) { 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, i18n.LangZhCN); ok { t.Fatal("只写 start 且尚未到点时应不展示") } 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, i18n.LangZhCN); !ok { t.Fatal("只写 end 且未到点时应立即展示") } 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), 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) { 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 应可解析") } want := time.Date(2026, 10, 24, 18, 0, 0, 0, time.UTC) if !at.Equal(want) { t.Fatalf("解析结果 = %s, want %s", at.UTC(), want) } 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", loc); !ok || at.Second() != 59 { t.Fatalf("秒级格式应可解析,got %s ok=%v", at, ok) } if _, ok := parseAnnouncementTime("2026/10/25 02:00", loc); ok { t.Fatal("错误格式应解析失败") } 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 }{ {"start 格式错", "2026/10/25 02:00", ""}, {"end 格式错", "", "10月26日"}, {"end 早于 start", "2026-10-26 00:00", "2026-10-25 00:00"}, {"end 等于 start", "2026-10-25 00:00", "2026-10-25 00:00"}, } { t.Run(c.name, func(t *testing.T) { setAnnouncement(t, "内容", c.start, c.end) if Announcement.Content != "" { t.Fatal("配置无效时公告应被关闭") } if !Announcement.StartAt.IsZero() || !Announcement.EndAt.IsZero() { t.Fatal("配置无效时应清空已解析的时间点") } }) } }