244 lines
8.1 KiB
Go
244 lines
8.1 KiB
Go
// 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("配置无效时应清空已解析的时间点")
|
||
}
|
||
})
|
||
}
|
||
}
|