test(loadbalance,security): 完善模块测试覆盖率
loadbalance 模块覆盖率从 52.4% 提升至 96.1%: - TestConsistentHash: 一致性哈希算法测试 - TestIsValidAlgorithm: 算法验证函数测试 security 模块覆盖率从 50.2% 提升至 63.7%: - TestNewSlidingWindowLimiter: 滑动窗口限流器创建测试 - TestSlidingWindowLimiter_Allow: 请求允许/拒绝测试 - TestSlidingWindowLimiter_Reset/ResetAll: 重置测试 - TestSlidingWindowLimiter_Cleanup: 清理测试 - TestSlidingWindowLimiter_GetStats/GetCount: 统计测试 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0a50678b55
commit
ff260def3b
@ -650,3 +650,131 @@ func TestBalancerInterface(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConsistentHash 测试一致性哈希负载均衡器。
|
||||||
|
func TestConsistentHash(t *testing.T) {
|
||||||
|
t.Run("创建默认配置", func(t *testing.T) {
|
||||||
|
ch := NewConsistentHash(0, "ip")
|
||||||
|
if ch == nil {
|
||||||
|
t.Fatal("NewConsistentHash() = nil")
|
||||||
|
}
|
||||||
|
if ch.GetVirtualNodes() != 150 {
|
||||||
|
t.Errorf("GetVirtualNodes() = %d, want 150", ch.GetVirtualNodes())
|
||||||
|
}
|
||||||
|
if ch.GetHashKey() != "ip" {
|
||||||
|
t.Errorf("GetHashKey() = %q, want %q", ch.GetHashKey(), "ip")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("自定义虚拟节点数", func(t *testing.T) {
|
||||||
|
ch := NewConsistentHash(200, "uri")
|
||||||
|
if ch.GetVirtualNodes() != 200 {
|
||||||
|
t.Errorf("GetVirtualNodes() = %d, want 200", ch.GetVirtualNodes())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SelectByKey 空目标", func(t *testing.T) {
|
||||||
|
ch := NewConsistentHash(150, "ip")
|
||||||
|
got := ch.SelectByKey([]*Target{}, "192.168.1.1")
|
||||||
|
if got != nil {
|
||||||
|
t.Errorf("SelectByKey() = %v, want nil", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SelectByKey 单目标", func(t *testing.T) {
|
||||||
|
ch := NewConsistentHash(150, "ip")
|
||||||
|
targets := []*Target{
|
||||||
|
createHealthyTarget("http://backend1:8080", true),
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ch.SelectByKey(targets, "192.168.1.1")
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("SelectByKey() = nil")
|
||||||
|
}
|
||||||
|
if got.URL != "http://backend1:8080" {
|
||||||
|
t.Errorf("SelectByKey() = %q, want %q", got.URL, "http://backend1:8080")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SelectByKey 多目标相同键", func(t *testing.T) {
|
||||||
|
ch := NewConsistentHash(150, "ip")
|
||||||
|
targets := []*Target{
|
||||||
|
createHealthyTarget("http://backend1:8080", true),
|
||||||
|
createHealthyTarget("http://backend2:8080", true),
|
||||||
|
createHealthyTarget("http://backend3:8080", true),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相同的键应该选择相同的目标
|
||||||
|
key := "192.168.1.100"
|
||||||
|
first := ch.SelectByKey(targets, key)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
got := ch.SelectByKey(targets, key)
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("SelectByKey() = nil")
|
||||||
|
}
|
||||||
|
if got.URL != first.URL {
|
||||||
|
t.Errorf("相同键选择不同目标: first=%q, got=%q", first.URL, got.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetStats", func(t *testing.T) {
|
||||||
|
ch := NewConsistentHash(100, "ip")
|
||||||
|
targets := []*Target{
|
||||||
|
createHealthyTarget("http://backend1:8080", true),
|
||||||
|
createHealthyTarget("http://backend2:8080", true),
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.Rebuild(targets)
|
||||||
|
stats := ch.GetStats()
|
||||||
|
|
||||||
|
if stats.VirtualNodes != 100 {
|
||||||
|
t.Errorf("VirtualNodes = %d, want 100", stats.VirtualNodes)
|
||||||
|
}
|
||||||
|
if stats.CircleSize != 200 { // 2 targets * 100 nodes
|
||||||
|
t.Errorf("CircleSize = %d, want 200", stats.CircleSize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Rebuild 跳过不健康目标", func(t *testing.T) {
|
||||||
|
ch := NewConsistentHash(10, "ip")
|
||||||
|
targets := []*Target{
|
||||||
|
createHealthyTarget("http://backend1:8080", false),
|
||||||
|
createHealthyTarget("http://backend2:8080", true),
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.Rebuild(targets)
|
||||||
|
stats := ch.GetStats()
|
||||||
|
|
||||||
|
if stats.CircleSize != 10 { // 只有1个健康目标 * 10 nodes
|
||||||
|
t.Errorf("CircleSize = %d, want 10", stats.CircleSize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsValidAlgorithm 测试算法验证函数。
|
||||||
|
func TestIsValidAlgorithm(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
algorithm string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"round_robin", "round_robin", true},
|
||||||
|
{"weighted_round_robin", "weighted_round_robin", true},
|
||||||
|
{"least_conn", "least_conn", true},
|
||||||
|
{"ip_hash", "ip_hash", true},
|
||||||
|
{"consistent_hash", "consistent_hash", true},
|
||||||
|
{"invalid", "invalid", false},
|
||||||
|
{"empty", "", true}, // 空字符串有效(使用默认值)
|
||||||
|
{"unknown", "unknown-algorithm", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := IsValidAlgorithm(tt.algorithm)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("IsValidAlgorithm(%q) = %v, want %v", tt.algorithm, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
179
internal/middleware/security/sliding_window_test.go
Normal file
179
internal/middleware/security/sliding_window_test.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSlidingWindowLimiter(t *testing.T) {
|
||||||
|
t.Run("默认配置", func(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 100, false)
|
||||||
|
if limiter == nil {
|
||||||
|
t.Fatal("NewSlidingWindowLimiter() = nil")
|
||||||
|
}
|
||||||
|
if limiter.window != time.Second {
|
||||||
|
t.Errorf("window = %v, want %v", limiter.window, time.Second)
|
||||||
|
}
|
||||||
|
if limiter.limit != 100 {
|
||||||
|
t.Errorf("limit = %d, want 100", limiter.limit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("精确模式", func(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Minute, 50, true)
|
||||||
|
if !limiter.precise {
|
||||||
|
t.Error("precise should be true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingWindowLimiter_Allow(t *testing.T) {
|
||||||
|
t.Run("近似模式-允许请求", func(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 10, false)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if !limiter.Allow("test-key") {
|
||||||
|
t.Errorf("请求 %d 应该被允许", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("近似模式-拒绝超限请求", func(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 5, false)
|
||||||
|
|
||||||
|
// 发送 5 个请求
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
limiter.Allow("test-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证统计
|
||||||
|
stats := limiter.GetStats()
|
||||||
|
if stats.Limit != 5 {
|
||||||
|
t.Errorf("Limit = %d, want 5", stats.Limit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("精确模式-允许请求", func(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 10, true)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if !limiter.Allow("test-key") {
|
||||||
|
t.Errorf("请求 %d 应该被允许", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("精确模式-拒绝超限请求", func(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 3, true)
|
||||||
|
|
||||||
|
// 发送 3 个请求
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if !limiter.Allow("test-key") {
|
||||||
|
t.Errorf("请求 %d 应该被允许", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第 4 个请求应该被拒绝
|
||||||
|
if limiter.Allow("test-key") {
|
||||||
|
t.Error("第 4 个请求应该被拒绝")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("不同键独立计数", func(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 2, true)
|
||||||
|
|
||||||
|
// key1 发送 2 个请求
|
||||||
|
limiter.Allow("key1")
|
||||||
|
limiter.Allow("key1")
|
||||||
|
|
||||||
|
// key2 应该仍可发送
|
||||||
|
if !limiter.Allow("key2") {
|
||||||
|
t.Error("key2 的请求应该被允许")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingWindowLimiter_Reset(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 5, true)
|
||||||
|
|
||||||
|
// 发送一些请求
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
limiter.Allow("test-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
limiter.Reset("test-key")
|
||||||
|
|
||||||
|
// 验证计数归零
|
||||||
|
if count := limiter.GetCount("test-key"); count != 0 {
|
||||||
|
t.Errorf("GetCount() = %d, want 0 after reset", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingWindowLimiter_ResetAll(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 5, true)
|
||||||
|
|
||||||
|
// 为多个键发送请求
|
||||||
|
limiter.Allow("key1")
|
||||||
|
limiter.Allow("key2")
|
||||||
|
limiter.Allow("key3")
|
||||||
|
|
||||||
|
// 重置所有
|
||||||
|
limiter.ResetAll()
|
||||||
|
|
||||||
|
stats := limiter.GetStats()
|
||||||
|
if stats.CounterKeys != 0 {
|
||||||
|
t.Errorf("CounterKeys = %d, want 0 after ResetAll", stats.CounterKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingWindowLimiter_Cleanup(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 5, true)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
limiter.Allow("test-key")
|
||||||
|
|
||||||
|
// 清理(由于请求刚发送,不应该被清理)
|
||||||
|
limiter.Cleanup(time.Minute)
|
||||||
|
|
||||||
|
stats := limiter.GetStats()
|
||||||
|
if stats.CounterKeys != 1 {
|
||||||
|
t.Errorf("CounterKeys = %d, want 1", stats.CounterKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingWindowLimiter_GetStats(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Minute, 100, false)
|
||||||
|
|
||||||
|
stats := limiter.GetStats()
|
||||||
|
|
||||||
|
if stats.Window != time.Minute {
|
||||||
|
t.Errorf("Window = %v, want %v", stats.Window, time.Minute)
|
||||||
|
}
|
||||||
|
if stats.Limit != 100 {
|
||||||
|
t.Errorf("Limit = %d, want 100", stats.Limit)
|
||||||
|
}
|
||||||
|
if stats.Precise {
|
||||||
|
t.Error("Precise should be false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingWindowLimiter_GetCount(t *testing.T) {
|
||||||
|
limiter := NewSlidingWindowLimiter(time.Second, 10, true)
|
||||||
|
|
||||||
|
// 发送 3 个请求
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
limiter.Allow("test-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
count := limiter.GetCount("test-key")
|
||||||
|
if count != 3 {
|
||||||
|
t.Errorf("GetCount() = %d, want 3", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不存在的键
|
||||||
|
count = limiter.GetCount("nonexistent")
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("GetCount(nonexistent) = %d, want 0", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user