Files
tamabox/internal/db/feature_control_test.go
T
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00

342 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
})
}