发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
This commit is contained in:
@@ -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("最早锁定条目未被驱逐")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user