From ff260def3b60a4de5b339b34cf392ab4376aec0e Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 3 Apr 2026 18:38:35 +0800 Subject: [PATCH] =?UTF-8?q?test(loadbalance,security):=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=A8=A1=E5=9D=97=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/loadbalance/balancer_test.go | 128 +++++++++++++ .../security/sliding_window_test.go | 179 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 internal/middleware/security/sliding_window_test.go diff --git a/internal/loadbalance/balancer_test.go b/internal/loadbalance/balancer_test.go index d680856..1ac5c06 100644 --- a/internal/loadbalance/balancer_test.go +++ b/internal/loadbalance/balancer_test.go @@ -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) + } + }) + } +} diff --git a/internal/middleware/security/sliding_window_test.go b/internal/middleware/security/sliding_window_test.go new file mode 100644 index 0000000..0278565 --- /dev/null +++ b/internal/middleware/security/sliding_window_test.go @@ -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) + } +} \ No newline at end of file