lolly/internal/proxy/health_test.go
xfy 1ce84ce9c5 refactor(proxy): 提取命名常量并适配变量系统重命名
提取硬编码字符串为命名常量:
- upstreamCache = "CACHE"
- protoHTTPS = "https"
ProxyWebSocket → WebSocket
适配 variable.Context 重命名

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 09:40:37 +08:00

447 lines
11 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")
}
})
}