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("最早锁定条目未被驱逐") } }