// 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" ) // setAnnouncement 在测试期间覆盖全局公告配置,结束后恢复。 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() } func mustParse(t *testing.T, layout, value string) time.Time { t.Helper() parsed, err := time.ParseInLocation(layout, value, time.Local) 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()); ok { t.Fatal("content 为空时公告应关闭") } } 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") 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) 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 := time.Date(2026, 10, 22, 12, 0, 0, 0, time.Local) setAnnouncement(t, "只有开始", "2026-10-23 00:00", "") if _, ok := Announcement.Active(now); ok { t.Fatal("只写 start 且尚未到点时应不展示") } if _, ok := Announcement.Active(now.Add(48 * time.Hour)); !ok { t.Fatal("只写 start 时到点后应一直展示") } setAnnouncement(t, "只有结束", "", "2026-10-23 00:00") if _, ok := Announcement.Active(now); !ok { t.Fatal("只写 end 且未到点时应立即展示") } if _, ok := Announcement.Active(now.Add(48 * time.Hour)); ok { t.Fatal("只写 end 时过点后应下线") } setAnnouncement(t, "一直展示", "", "") if _, ok := Announcement.Active(now.AddDate(5, 0, 0)); !ok { t.Fatal("起止都留空时应一直展示") } } func TestAnnouncementTimeFormats(t *testing.T) { // 带偏移的 RFC3339 必须按字面时刻解析,而不是再套一层本地时区。 at, ok := parseAnnouncementTime("2026-10-25T02:00:00+08:00") 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"); !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 { t.Fatalf("秒级格式应可解析,got %s ok=%v", at, ok) } if _, ok := parseAnnouncementTime("2026/10/25 02:00"); ok { t.Fatal("错误格式应解析失败") } if _, ok := parseAnnouncementTime("2026-13-01"); ok { 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("配置无效时应清空已解析的时间点") } }) } }