lolly/internal/proxy/health_test.go
xfy 8f79fb6797 test(config,handler,loadbalance,proxy): 扩展单元测试覆盖率
添加以下测试:
- validate_test.go: Rewrite、NextUpstream、DefaultServer、Mode、
  ListenConflicts、HTTP2、RedirectRewrite 验证测试
- sendfile_test.go: 无效文件描述符、零长度传输、部分传输、
  带偏移量传输测试
- balancer_test.go: ConsistentHash Select/SelectExcluding、
  RandomBalancer 边界条件和 Power of Two Choices 测试
- health_test.go: MarkHealthy/MarkUnhealthy 与 SlowStartManager
  集成测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 18:28:28 +08:00

504 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package proxy 提供健康检查功能的测试。
//
// 该文件测试健康检查模块的各项功能,包括:
// - 健康检查器创建
// - 默认值应用
// - 自定义配置
// - 负值配置处理
// - 零值配置处理
// - 启动和停止控制
// - 目标健康检查
// - 超时处理
// - 连接失败处理
// - 标记不健康
//
// 作者xfy
package proxy
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/loadbalance"
)
// TestNewHealthChecker 测试 NewHealthChecker 函数。
func TestNewHealthChecker(t *testing.T) {
t.Run("默认值应用", func(t *testing.T) {
targets := []*loadbalance.Target{
{URL: "http://backend1:8080"},
}
targets[0].Healthy.Store(true)
cfg := &config.HealthCheckConfig{}
checker := NewHealthChecker(targets, cfg)
if checker.GetInterval() != 10*time.Second {
t.Errorf("Interval = %v, want %v", checker.GetInterval(), 10*time.Second)
}
if checker.GetTimeout() != 5*time.Second {
t.Errorf("Timeout = %v, want %v", checker.GetTimeout(), 5*time.Second)
}
if checker.GetPath() != healthPath {
t.Errorf("Path = %q, want %q", checker.GetPath(), healthPath)
}
if checker.IsRunning() {
t.Error("新建的 checker 应未启动")
}
})
t.Run("自定义配置", func(t *testing.T) {
targets := []*loadbalance.Target{
{URL: "http://backend1:8080"},
{URL: "http://backend2:8080"},
}
targets[0].Healthy.Store(true)
targets[1].Healthy.Store(true)
cfg := &config.HealthCheckConfig{
Interval: 30 * time.Second,
Timeout: 10 * time.Second,
Path: "/status",
}
checker := NewHealthChecker(targets, cfg)
if checker.GetInterval() != 30*time.Second {
t.Errorf("Interval = %v, want %v", checker.GetInterval(), 30*time.Second)
}
if checker.GetTimeout() != 10*time.Second {
t.Errorf("Timeout = %v, want %v", checker.GetTimeout(), 10*time.Second)
}
if checker.GetPath() != "/status" {
t.Errorf("Path = %q, want %q", checker.GetPath(), "/status")
}
})
t.Run("负值配置使用默认值", func(t *testing.T) {
targets := []*loadbalance.Target{
{URL: "http://backend1:8080"},
}
targets[0].Healthy.Store(true)
cfg := &config.HealthCheckConfig{
Interval: -1 * time.Second,
Timeout: -1 * time.Second,
}
checker := NewHealthChecker(targets, cfg)
if checker.GetInterval() != 10*time.Second {
t.Errorf("负值 Interval 应使用默认值got %v", checker.GetInterval())
}
if checker.GetTimeout() != 5*time.Second {
t.Errorf("负值 Timeout 应使用默认值got %v", checker.GetTimeout())
}
})
t.Run("零值配置使用默认值", func(t *testing.T) {
targets := []*loadbalance.Target{
{URL: "http://backend1:8080"},
}
targets[0].Healthy.Store(true)
cfg := &config.HealthCheckConfig{
Interval: 0,
Timeout: 0,
Path: "",
}
checker := NewHealthChecker(targets, cfg)
if checker.GetInterval() != 10*time.Second {
t.Errorf("零值 Interval 应使用默认值got %v", checker.GetInterval())
}
if checker.GetTimeout() != 5*time.Second {
t.Errorf("零值 Timeout 应使用默认值got %v", checker.GetTimeout())
}
if checker.GetPath() != healthPath {
t.Errorf("空 Path 应使用默认值got %q", checker.GetPath())
}
})
}
// TestHealthCheckerStartStop 测试 Start 和 Stop 方法。
func TestHealthCheckerStartStop(t *testing.T) {
t.Run("启动和停止", func(t *testing.T) {
targets := []*loadbalance.Target{
{URL: "http://backend1:8080"},
}
targets[0].Healthy.Store(true)
cfg := &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
}
checker := NewHealthChecker(targets, cfg)
if checker.IsRunning() {
t.Error("启动前 IsRunning 应返回 false")
}
checker.Start()
if !checker.IsRunning() {
t.Error("启动后 IsRunning 应返回 true")
}
checker.Stop()
if checker.IsRunning() {
t.Error("停止后 IsRunning 应返回 false")
}
})
t.Run("重复启动无效果", func(t *testing.T) {
targets := []*loadbalance.Target{
{URL: "http://backend1:8080"},
}
targets[0].Healthy.Store(true)
cfg := &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
}
checker := NewHealthChecker(targets, cfg)
checker.Start()
checker.Start()
if !checker.IsRunning() {
t.Error("重复启动后 checker 应仍在运行")
}
checker.Stop()
})
t.Run("重复停止无效果", func(t *testing.T) {
targets := []*loadbalance.Target{
{URL: "http://backend1:8080"},
}
targets[0].Healthy.Store(true)
cfg := &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
}
checker := NewHealthChecker(targets, cfg)
checker.Stop()
checker.Stop()
if checker.IsRunning() {
t.Error("未启动时停止checker 应不在运行")
}
})
}
// TestCheckTarget 测试 checkTarget 方法。
func TestCheckTarget(t *testing.T) {
t.Run("健康响应", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != healthPath {
t.Errorf("请求路径 = %q, want %q", r.URL.Path, healthPath)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
target := &loadbalance.Target{
URL: server.URL,
}
target.Healthy.Store(false)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.checkTarget(target)
if !target.Healthy.Load() {
t.Error("健康响应后 target 应标记为 healthy")
}
})
t.Run("不健康响应", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
target := &loadbalance.Target{
URL: server.URL,
}
target.Healthy.Store(true)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.checkTarget(target)
if target.Healthy.Load() {
t.Error("5xx 响应后 target 应标记为 unhealthy")
}
})
t.Run("超时", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
time.Sleep(100 * time.Millisecond)
}))
defer server.Close()
target := &loadbalance.Target{
URL: server.URL,
}
target.Healthy.Store(true)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 10 * time.Millisecond,
Path: healthPath,
})
checker.checkTarget(target)
if target.Healthy.Load() {
t.Error("超时后 target 应标记为 unhealthy")
}
})
t.Run("连接失败", func(t *testing.T) {
target := &loadbalance.Target{
URL: "http://invalid-host-that-does-not-exist:99999",
}
target.Healthy.Store(true)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 100 * time.Millisecond,
Path: healthPath,
})
checker.checkTarget(target)
if target.Healthy.Load() {
t.Error("连接失败后 target 应标记为 unhealthy")
}
})
t.Run("3xx 重定向响应", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusMovedPermanently)
}))
defer server.Close()
target := &loadbalance.Target{
URL: server.URL,
}
target.Healthy.Store(true)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.checkTarget(target)
if target.Healthy.Load() {
t.Error("3xx 响应后 target 应标记为 unhealthy")
}
})
t.Run("4xx 客户端错误响应", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
target := &loadbalance.Target{
URL: server.URL,
}
target.Healthy.Store(true)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.checkTarget(target)
if target.Healthy.Load() {
t.Error("4xx 响应后 target 应标记为 unhealthy")
}
})
t.Run("2xx 成功响应", func(t *testing.T) {
tests := []struct {
name string
statusCode int
}{
{"200 OK", http.StatusOK},
{"201 Created", http.StatusCreated},
{"204 No Content", http.StatusNoContent},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(tt.statusCode)
}))
defer server.Close()
target := &loadbalance.Target{
URL: server.URL,
}
target.Healthy.Store(false)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.checkTarget(target)
if !target.Healthy.Load() {
t.Errorf("%d 响应后 target 应标记为 healthy", tt.statusCode)
}
})
}
})
}
// TestMarkUnhealthy 测试 MarkUnhealthy 方法。
func TestMarkUnhealthy(t *testing.T) {
t.Run("标记不健康", func(t *testing.T) {
target := &loadbalance.Target{
URL: "http://backend1:8080",
}
target.Healthy.Store(true)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.MarkUnhealthy(target)
if target.Healthy.Load() {
t.Error("MarkUnhealthy 后 target 应标记为 unhealthy")
}
})
t.Run("已不健康的 target 再次标记", func(t *testing.T) {
target := &loadbalance.Target{
URL: "http://backend1:8080",
}
target.Healthy.Store(false)
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.MarkUnhealthy(target)
if target.Healthy.Load() {
t.Error("MarkUnhealthy 后 target 应保持 unhealthy 状态")
}
})
t.Run("多 target 场景", func(t *testing.T) {
target1 := &loadbalance.Target{
URL: "http://backend1:8080",
}
target1.Healthy.Store(true)
target2 := &loadbalance.Target{
URL: "http://backend2:8080",
}
target2.Healthy.Store(true)
checker := NewHealthChecker([]*loadbalance.Target{target1, target2}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: healthPath,
})
checker.MarkUnhealthy(target1)
if target1.Healthy.Load() {
t.Error("target1 应标记为 unhealthy")
}
if !target2.Healthy.Load() {
t.Error("target2 应保持 healthy")
}
})
}
// TestMarkUnhealthy_WithSlowStartManager 测试 MarkUnhealthy 与 SlowStartManager 集成。
func TestMarkUnhealthy_WithSlowStartManager(t *testing.T) {
target := &loadbalance.Target{
URL: "http://127.0.0.1:8080",
Weight: 100,
}
target.Healthy.Store(true)
target.SlowStart = 30 * time.Second
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: "/health",
SlowStart: 30 * time.Second,
})
// 先标记为健康以初始化慢启动
checker.MarkHealthy(target)
// 标记目标为不健康
checker.MarkUnhealthy(target)
if target.Healthy.Load() {
t.Error("target 应标记为 unhealthy")
}
}
// TestMarkHealthy_WithSlowStartManager 测试 MarkHealthy 与 SlowStartManager 集成。
func TestMarkHealthy_WithSlowStartManager(t *testing.T) {
target := &loadbalance.Target{
URL: "http://127.0.0.1:8080",
Weight: 100,
}
target.Healthy.Store(false)
target.SlowStart = 30 * time.Second
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
Interval: 1 * time.Hour,
Timeout: 5 * time.Second,
Path: "/health",
SlowStart: 30 * time.Second,
})
// 标记目标为健康
checker.MarkHealthy(target)
if !target.Healthy.Load() {
t.Error("target 应标记为 healthy")
}
// 验证慢启动已开始EffectiveWeight 应被设置为 1
ew := target.EffectiveWeight.Load()
if ew <= 0 {
t.Errorf("慢启动 EffectiveWeight 应大于 0got: %d", ew)
}
}