0904
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user