- config: 为 Config 和所有子配置结构添加完整文档,包含使用示例和注意事项 - stream: 为负载均衡器和服务器添加详细的参数、返回值和功能说明 - logging: 为日志格式化和输出函数添加文档,说明支持的变量替换 - handler: 为路由器、静态文件和 sendfile 处理器添加文档 - proxy: 为健康检查器和代理功能添加完整文档 - cache/server/ssl/middleware: 补充相关模块的文档注释 - config.example.yaml: 添加可信代理配置、加密套件示例,更新压缩级别说明 Co-Authored-By: Claude <noreply@anthropic.com>
447 lines
11 KiB
Go
447 lines
11 KiB
Go
// 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() != "/health" {
|
||
t.Errorf("Path = %q, want %q", checker.GetPath(), "/health")
|
||
}
|
||
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() != "/health" {
|
||
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: "/health",
|
||
}
|
||
|
||
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 != "/health" {
|
||
t.Errorf("请求路径 = %q, want %q", r.URL.Path, "/health")
|
||
}
|
||
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: "/health",
|
||
})
|
||
|
||
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, r *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: "/health",
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("5xx 响应后 target 应标记为 unhealthy")
|
||
}
|
||
})
|
||
|
||
t.Run("超时", func(t *testing.T) {
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *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: "/health",
|
||
})
|
||
|
||
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: "/health",
|
||
})
|
||
|
||
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, r *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: "/health",
|
||
})
|
||
|
||
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, r *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: "/health",
|
||
})
|
||
|
||
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, r *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: "/health",
|
||
})
|
||
|
||
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: "/health",
|
||
})
|
||
|
||
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: "/health",
|
||
})
|
||
|
||
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: "/health",
|
||
})
|
||
|
||
checker.MarkUnhealthy(target1)
|
||
|
||
if target1.Healthy.Load() {
|
||
t.Error("target1 应标记为 unhealthy")
|
||
}
|
||
if !target2.Healthy.Load() {
|
||
t.Error("target2 应保持 healthy")
|
||
}
|
||
})
|
||
}
|