发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
CI / test (push) Successful in 30s
Release / release (push) Successful in 49s

This commit is contained in:
Wang Defa
2026-07-10 17:38:34 +08:00
parent 4af6a0ca92
commit dbba1f4905
78 changed files with 6898 additions and 551 deletions
+105
View File
@@ -0,0 +1,105 @@
package service
import (
"fmt"
"strings"
"testing"
"time"
)
// TestGuardKeyNormalization 用户名去空白并按字节截断,高基数超长输入收敛。
func TestGuardKeyNormalization(t *testing.T) {
tests := []struct {
name string
ip string
user string
want string
}{
{name: "常规", ip: "1.2.3.4", user: "admin", want: "1.2.3.4|admin"},
{name: "去空白", ip: "1.2.3.4", user: " admin ", want: "1.2.3.4|admin"},
{name: "超长截断", ip: "1.2.3.4", user: strings.Repeat("a", 100), want: "1.2.3.4|" + strings.Repeat("a", guardUserMax)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := guardKey(tt.ip, tt.user); got != tt.want {
t.Errorf("guardKey = %q, want %q", got, tt.want)
}
})
}
}
// TestGuardCapacityBounded 高基数唯一 key 持续失败,条目数不超过上限。
func TestGuardCapacityBounded(t *testing.T) {
g := newLoginGuard()
now := time.Now()
for i := 0; i < maxGuardEntries*2; i++ {
g.fail(fmt.Sprintf("1.2.3.4|user-%d", i), now, 100, time.Hour)
}
if n := len(g.failures); n > maxGuardEntries {
t.Errorf("failures 条目 = %d, want ≤ %d", n, maxGuardEntries)
}
}
// TestGuardCapacitySweepExpired 容量满时优先清理窗口外条目,而非驱逐活跃条目。
func TestGuardCapacitySweepExpired(t *testing.T) {
g := newLoginGuard()
base := time.Now()
window := time.Hour
// 填满:全部为「早已过窗」的旧失败
for i := 0; i < maxGuardEntries; i++ {
g.fail(fmt.Sprintf("old|%d", i), base.Add(-2*window), 100, window)
}
// 新失败触发清理:过期条目被清,新条目正常记录
g.fail("fresh|user", base, 100, window)
if _, ok := g.failures["fresh|user"]; !ok {
t.Error("新条目未记录")
}
if n := len(g.failures); n > maxGuardEntries {
t.Errorf("清理后条目 = %d, want ≤ %d", n, maxGuardEntries)
}
}
// TestGuardEvictOldestKeepsNewest 无过期可清时驱逐最旧,最新条目保留。
func TestGuardEvictOldestKeepsNewest(t *testing.T) {
g := newLoginGuard()
base := time.Now()
window := 24 * time.Hour
for i := 0; i < maxGuardEntries; i++ {
// 均在窗口内,时间递增;user-0 最旧
g.fail(fmt.Sprintf("ip|user-%d", i), base.Add(time.Duration(i)*time.Second), 100, window)
}
g.fail("ip|newest", base.Add(time.Duration(maxGuardEntries+1)*time.Second), 100, window)
if _, ok := g.failures["ip|newest"]; !ok {
t.Error("最新条目未记录")
}
if _, ok := g.failures["ip|user-0"]; ok {
t.Error("最旧条目未被驱逐")
}
}
// TestGuardLockCapacityBounded 高基数唯一 key 全部打满阈值转入锁定,
// 锁定期内(无可清理的过期锁)条目数仍不超过上限,最新锁保留、最早锁被驱逐。
func TestGuardLockCapacityBounded(t *testing.T) {
g := newLoginGuard()
base := time.Now()
window := 15 * time.Minute
total := maxGuardEntries + 100
for i := 0; i < total; i++ {
key := fmt.Sprintf("1.2.3.4|user-%d", i)
// 每个 key 打满 2 次(阈值)即转入锁定;时间递增,i 越小锁定越早
at := base.Add(time.Duration(i) * time.Millisecond)
g.fail(key, at, 2, window)
if !g.fail(key, at, 2, window) {
t.Fatalf("key %s 未按阈值转入锁定", key)
}
}
if n := len(g.lockedAt); n > maxGuardEntries {
t.Errorf("lockedAt 条目 = %d, want ≤ %d", n, maxGuardEntries)
}
if _, ok := g.lockedAt[fmt.Sprintf("1.2.3.4|user-%d", total-1)]; !ok {
t.Error("最新锁定条目未保留")
}
if _, ok := g.lockedAt["1.2.3.4|user-0"]; ok {
t.Error("最早锁定条目未被驱逐")
}
}