// 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 db import ( "testing" "time" ) // TestFeatureControlEvaluate 覆盖开关 / 定时 / 定量三类控制的组合与优先级。 // 纯逻辑测试,不依赖外部数据库。 func TestFeatureControlEvaluate(t *testing.T) { now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC) past := now.Add(-time.Hour) future := now.Add(time.Hour) tests := []struct { name string fc FeatureControl wantOk bool want string }{ { name: "开启且无限制", fc: FeatureControl{Enabled: true}, wantOk: true, want: "", }, { name: "手动关闭优先于其它条件", fc: FeatureControl{Enabled: false, OpenAt: &past, CloseAt: &future, QuotaLimit: 1}, wantOk: false, want: FeatureReasonDisabled, }, { name: "未到开放时间", fc: FeatureControl{Enabled: true, OpenAt: &future}, wantOk: false, want: FeatureReasonNotStarted, }, { name: "开放时间已过但功能仍开启", fc: FeatureControl{Enabled: true, OpenAt: &past}, wantOk: true, want: "", }, { name: "已过关闭时间", fc: FeatureControl{Enabled: true, OpenAt: &past, CloseAt: &past}, wantOk: false, want: FeatureReasonEnded, }, { name: "刚好到达关闭时间即关闭", fc: FeatureControl{Enabled: true, CloseAt: &now}, wantOk: false, want: FeatureReasonEnded, }, { name: "未到收集上限", fc: FeatureControl{Enabled: true, QuotaLimit: 10, QuotaCount: 9}, wantOk: true, want: "", }, { name: "恰好达到收集上限", fc: FeatureControl{Enabled: true, QuotaLimit: 10, QuotaCount: 10}, wantOk: false, want: FeatureReasonQuotaReached, }, { name: "超过收集上限", fc: FeatureControl{Enabled: true, QuotaLimit: 10, QuotaCount: 12}, wantOk: false, want: FeatureReasonQuotaReached, }, { name: "上限为 0 表示不限制", fc: FeatureControl{Enabled: true, QuotaLimit: 0, QuotaCount: 9999}, wantOk: true, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { st := tt.fc.Evaluate(now) if st.Open != tt.wantOk || st.Reason != tt.want { t.Fatalf("Evaluate() = (open=%v, reason=%q), want (open=%v, reason=%q)", st.Open, st.Reason, tt.wantOk, tt.want) } }) } } // TestDefaultFeatureControl 私聊默认关闭(沿用 users.allow_chat 的历史默认), // 提问与投票默认开启,保证功能上线不改变既有行为。 func TestDefaultFeatureControl(t *testing.T) { cases := map[string]bool{ FeatureQuestion: true, FeatureChat: false, FeatureVote: true, } for feature, want := range cases { if got := DefaultFeatureControl(1, feature).Enabled; got != want { t.Fatalf("DefaultFeatureControl(%q).Enabled = %v, want %v", feature, got, want) } } } func TestIsValidFeature(t *testing.T) { for _, k := range FeatureKeys { if !IsValidFeature(k) { t.Fatalf("IsValidFeature(%q) = false, want true", k) } } for _, k := range []string{"", "chat ", "CHAT", "poll", "messages"} { if IsValidFeature(k) { t.Fatalf("IsValidFeature(%q) = true, want false", k) } } } // TestFeatureControlSchedule 覆盖循环定时(每周开放日 + 每日时间段)的 Evaluate 判定。 func TestFeatureControlSchedule(t *testing.T) { // 固定在一个周四 12:00(time.Weekday: 0=周日…4=周四…6=周六) now := time.Date(2026, 9, 3, 12, 0, 0, 0, time.UTC) // 2026-09-03 是周四 thursday := int(now.Weekday()) if thursday != 4 { t.Fatalf("test precondition: 2026-09-03 should be Thursday, got %d", thursday) } tests := []struct { name string fc FeatureControl wantOk bool want string }{ { name: "无循环定时不影响判定", fc: FeatureControl{Enabled: true}, wantOk: true, want: "", }, { name: "每日时间段内开放", fc: FeatureControl{Enabled: true, ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: true, want: "", }, { name: "每日时间段开始前关闭", fc: FeatureControl{Enabled: true, ScheduleStart: "13:00", ScheduleEnd: "18:00"}, wantOk: false, want: FeatureReasonNotStarted, }, { name: "每日时间段结束后关闭(次日会再开)", fc: FeatureControl{Enabled: true, ScheduleStart: "09:00", ScheduleEnd: "11:00"}, wantOk: false, want: FeatureReasonNotStarted, }, { name: "当天不在开放日关闭(未来还会再开)", fc: FeatureControl{Enabled: true, ScheduleDays: "0", ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: false, want: FeatureReasonNotStarted, }, { name: "只设开放日(不设时间)表示全天", fc: FeatureControl{Enabled: true, ScheduleDays: "4"}, wantOk: true, want: "", }, { name: "只设时间(不设开放日)表示每天该时段", fc: FeatureControl{Enabled: true, ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: true, want: "", }, { name: "开放日与时间段组合:非开放日关闭", fc: FeatureControl{Enabled: true, ScheduleDays: "1,3,5", ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: false, want: FeatureReasonNotStarted, // 周四不在 1,3,5 中 }, { name: "开放日与时间段组合:开放日+时段内开放", fc: FeatureControl{Enabled: true, ScheduleDays: "2,4", ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: true, want: "", }, { name: "循环定时达到收集上限", fc: FeatureControl{Enabled: true, ScheduleDays: "4", ScheduleStart: "09:00", ScheduleEnd: "18:00", QuotaLimit: 5, QuotaCount: 5}, wantOk: false, want: FeatureReasonQuotaReached, }, { name: "手动关闭优先于循环定时", fc: FeatureControl{Enabled: false, ScheduleDays: "4", ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: false, want: FeatureReasonDisabled, }, { name: "一次性窗口与循环定时叠加:窗口内+时段内开放", fc: FeatureControl{Enabled: true, OpenAt: &[]time.Time{now.Add(-time.Hour)}[0], CloseAt: &[]time.Time{now.Add(24 * time.Hour)}[0], ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: true, want: "", }, { name: "一次性窗口已关闭后循环定时不再生效(硬边界)", fc: FeatureControl{Enabled: true, OpenAt: &[]time.Time{now.Add(-48 * time.Hour)}[0], CloseAt: &[]time.Time{now.Add(-24 * time.Hour)}[0], ScheduleDays: "4", ScheduleStart: "09:00", ScheduleEnd: "18:00"}, wantOk: false, want: FeatureReasonEnded, }, { name: "一次性窗口已关闭且无循环定时", fc: FeatureControl{Enabled: true, OpenAt: &[]time.Time{now.Add(-48 * time.Hour)}[0], CloseAt: &[]time.Time{now.Add(-24 * time.Hour)}[0]}, wantOk: false, want: FeatureReasonEnded, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { st := tt.fc.Evaluate(now) if st.Open != tt.wantOk || st.Reason != tt.want { t.Fatalf("Evaluate() = (open=%v, reason=%q), want (open=%v, reason=%q)", st.Open, st.Reason, tt.wantOk, tt.want) } }) } } // TestFeatureControlNextOpenAtLastCloseAt 覆盖「下次开放时间 / 上次关闭时间」的计算。 func TestFeatureControlNextOpenAtLastCloseAt(t *testing.T) { // 2026-09-03 12:00 UTC(周四) now := time.Date(2026, 9, 3, 12, 0, 0, 0, time.UTC) t.Run("循环定时与一次性窗口交集:当天时段内更晚开放", func(t *testing.T) { // now=周四 12:00,循环每天 09:00-18:00;一次性开放时间 = 今天 14:00 → 今天 14:00 开放 openAt := time.Date(2026, 9, 3, 14, 0, 0, 0, time.UTC) closeAt := time.Date(2026, 9, 3, 20, 0, 0, 0, time.UTC) fc := FeatureControl{Enabled: true, OpenAt: &openAt, CloseAt: &closeAt, ScheduleStart: "09:00", ScheduleEnd: "18:00"} st := fc.Evaluate(now) if st.Reason != FeatureReasonNotStarted { t.Fatalf("reason = %q, want not_started", st.Reason) } if st.NextOpenAt == nil || !st.NextOpenAt.Equal(openAt) { t.Fatalf("NextOpenAt = %v, want %v(今天 14:00)", st.NextOpenAt, openAt) } }) t.Run("一次性窗口在循环时段结束后才开放则当天不开放", func(t *testing.T) { // 一次性开放时间 20:00 晚于循环时段结束 18:00 → 今天无有效窗口,下次开放 = 明天 09:00 openAt := time.Date(2026, 9, 3, 20, 0, 0, 0, time.UTC) closeAt := time.Date(2026, 9, 5, 20, 0, 0, 0, time.UTC) fc := FeatureControl{Enabled: true, OpenAt: &openAt, CloseAt: &closeAt, ScheduleStart: "09:00", ScheduleEnd: "18:00"} st := fc.Evaluate(now) if st.Reason != FeatureReasonNotStarted { t.Fatalf("reason = %q, want not_started", st.Reason) } wantNext := time.Date(2026, 9, 4, 9, 0, 0, 0, time.UTC) if st.NextOpenAt == nil || !st.NextOpenAt.Equal(wantNext) { t.Fatalf("NextOpenAt = %v, want %v(明天 09:00)", st.NextOpenAt, wantNext) } }) t.Run("手动关闭与定量上限不展示时间信息", func(t *testing.T) { disabled := FeatureControl{Enabled: false, ScheduleStart: "09:00", ScheduleEnd: "18:00"} st := disabled.Evaluate(now) if st.Reason != FeatureReasonDisabled || st.NextOpenAt != nil || st.LastCloseAt != nil { t.Fatalf("disabled state = (reason=%q, next=%v, last=%v), want nil times", st.Reason, st.NextOpenAt, st.LastCloseAt) } quota := FeatureControl{Enabled: true, ScheduleStart: "09:00", ScheduleEnd: "18:00", QuotaLimit: 1, QuotaCount: 1} st = quota.Evaluate(now) if st.Reason != FeatureReasonQuotaReached || st.NextOpenAt != nil || st.LastCloseAt != nil { t.Fatalf("quota state = (reason=%q, next=%v, last=%v), want nil times", st.Reason, st.NextOpenAt, st.LastCloseAt) } }) t.Run("一次性窗口未到开放时间", func(t *testing.T) { openAt := now.Add(2 * time.Hour) closeAt := now.Add(24 * time.Hour) fc := FeatureControl{Enabled: true, OpenAt: &openAt, CloseAt: &closeAt} st := fc.Evaluate(now) if st.NextOpenAt == nil || !st.NextOpenAt.Equal(openAt) { t.Fatalf("NextOpenAt = %v, want %v", st.NextOpenAt, openAt) } if st.LastCloseAt != nil { t.Fatalf("LastCloseAt = %v, want nil(从未开放过)", st.LastCloseAt) } }) t.Run("一次性窗口已结束", func(t *testing.T) { openAt := now.Add(-48 * time.Hour) closeAt := now.Add(-24 * time.Hour) fc := FeatureControl{Enabled: true, OpenAt: &openAt, CloseAt: &closeAt} st := fc.Evaluate(now) if st.NextOpenAt != nil { t.Fatalf("NextOpenAt = %v, want nil(无循环定时,永久关闭)", st.NextOpenAt) } if st.LastCloseAt == nil || !st.LastCloseAt.Equal(closeAt) { t.Fatalf("LastCloseAt = %v, want %v", st.LastCloseAt, closeAt) } }) t.Run("循环定时:每天 09:00-18:00", func(t *testing.T) { fc := FeatureControl{Enabled: true, ScheduleStart: "09:00", ScheduleEnd: "18:00"} // 12:00 在窗口内:下次开放 = 明天 09:00;上次关闭 = 今天 18:00 尚未结束 → 昨天 18:00 st := fc.Evaluate(now) if st.Open != true { t.Fatalf("should be open, got reason=%q", st.Reason) } wantNext := time.Date(2026, 9, 4, 9, 0, 0, 0, time.UTC) if st.NextOpenAt == nil || !st.NextOpenAt.Equal(wantNext) { t.Fatalf("NextOpenAt = %v, want %v", st.NextOpenAt, wantNext) } wantLast := time.Date(2026, 9, 2, 18, 0, 0, 0, time.UTC) if st.LastCloseAt == nil || !st.LastCloseAt.Equal(wantLast) { t.Fatalf("LastCloseAt = %v, want %v", st.LastCloseAt, wantLast) } }) t.Run("循环定时:18:00 已过", func(t *testing.T) { evening := time.Date(2026, 9, 3, 20, 0, 0, 0, time.UTC) fc := FeatureControl{Enabled: true, ScheduleStart: "09:00", ScheduleEnd: "18:00"} st := fc.Evaluate(evening) if st.Open { t.Fatal("should be closed after daily end") } wantNext := time.Date(2026, 9, 4, 9, 0, 0, 0, time.UTC) if st.NextOpenAt == nil || !st.NextOpenAt.Equal(wantNext) { t.Fatalf("NextOpenAt = %v, want %v", st.NextOpenAt, wantNext) } wantLast := time.Date(2026, 9, 3, 18, 0, 0, 0, time.UTC) if st.LastCloseAt == nil || !st.LastCloseAt.Equal(wantLast) { t.Fatalf("LastCloseAt = %v, want %v", st.LastCloseAt, wantLast) } }) t.Run("循环定时:仅周五开放(今天周四)", func(t *testing.T) { fc := FeatureControl{Enabled: true, ScheduleDays: "5"} // 周五全天 st := fc.Evaluate(now) if st.Open { t.Fatal("should be closed on non-scheduled day") } wantNext := time.Date(2026, 9, 4, 0, 0, 0, 0, time.UTC) // 周五零点 if st.NextOpenAt == nil || !st.NextOpenAt.Equal(wantNext) { t.Fatalf("NextOpenAt = %v, want %v", st.NextOpenAt, wantNext) } // 全天窗口 [周五 00:00, 周六 00:00),最近一次关闭 = 上周六零点 wantLast := time.Date(2026, 8, 29, 0, 0, 0, 0, time.UTC) if st.LastCloseAt == nil || !st.LastCloseAt.Equal(wantLast) { t.Fatalf("LastCloseAt = %v, want %v", st.LastCloseAt, wantLast) } }) }