- Delete unused files: tempfile subsystem, matcher variants, server/internal - Remove 200+ unused functions across proxy, ssl, lua, http2/3, stream, variable - Fix proxy test type errors (backgroundRefresh ctx→Request) - Move bench/tools mock backend into internal/testutil - Remove corresponding test functions for all deleted code
719 lines
19 KiB
Go
719 lines
19 KiB
Go
// Package proxy 提供反向代理覆盖测试,补充 proxy.go 中未覆盖的方法。
|
||
//
|
||
// 该文件测试代理模块的以下功能:
|
||
// - selectTargetExcluding 排除已失败目标的选择
|
||
// - extractHashKey 哈希键提取
|
||
// - buildCacheKeyHash / buildCacheKeyHashValue 缓存键计算
|
||
// - writeCachedResponse 缓存响应写入
|
||
// - GetCache / GetCacheStats 缓存访问
|
||
// - getCacheDuration 不同状态码的缓存时间
|
||
// - redirect_rewrite 相关功能
|
||
//
|
||
// 作者:xfy
|
||
package proxy
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
"github.com/valyala/fasthttp/fasthttputil"
|
||
"rua.plus/lolly/internal/cache"
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/loadbalance"
|
||
"rua.plus/lolly/internal/testutil"
|
||
)
|
||
|
||
// TestSelectTargetExcluding 测试排除失败目标的目标选择
|
||
func TestSelectTargetExcluding(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://backend1:8080"},
|
||
{URL: "http://backend2:8080"},
|
||
{URL: "http://backend3:8080"},
|
||
}
|
||
for _, target := range targets {
|
||
target.Healthy.Store(true)
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
|
||
// 排除第一个目标,应该选择第二个
|
||
excluded := []*loadbalance.Target{targets[0]}
|
||
selected := p.selectTargetExcluding(ctx, excluded)
|
||
if selected == nil {
|
||
t.Fatal("selectTargetExcluding() returned nil")
|
||
}
|
||
if selected.URL == "http://backend1:8080" {
|
||
t.Error("selectTargetExcluding() should not select excluded target")
|
||
}
|
||
|
||
// 排除所有目标,应该返回 nil
|
||
allExcluded := []*loadbalance.Target{targets[0], targets[1], targets[2]}
|
||
selected = p.selectTargetExcluding(ctx, allExcluded)
|
||
if selected != nil {
|
||
t.Error("selectTargetExcluding() should return nil when all excluded")
|
||
}
|
||
|
||
// 空目标列表
|
||
p2, _ := NewProxy(cfg, []*loadbalance.Target{{URL: "http://a:1"}}, nil, nil)
|
||
p2.targets = nil
|
||
selected = p2.selectTargetExcluding(ctx, nil)
|
||
if selected != nil {
|
||
t.Error("selectTargetExcluding() should return nil for empty targets")
|
||
}
|
||
}
|
||
|
||
// TestSelectTargetExcluding_IPHash 测试 IP Hash 排除选择
|
||
func TestSelectTargetExcluding_IPHash(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "ip_hash",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://backend1:8080"},
|
||
{URL: "http://backend2:8080"},
|
||
{URL: "http://backend3:8080"},
|
||
}
|
||
for _, target := range targets {
|
||
target.Healthy.Store(true)
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtxWithHeader("GET", "/api/test", map[string]string{
|
||
"X-Forwarded-For": "192.168.1.1",
|
||
})
|
||
|
||
// 获取第一次选择
|
||
first := p.selectTarget(ctx)
|
||
if first == nil {
|
||
t.Fatal("selectTarget() returned nil")
|
||
}
|
||
|
||
// 排除第一次选择的目标
|
||
excluded := []*loadbalance.Target{first}
|
||
second := p.selectTargetExcluding(ctx, excluded)
|
||
if second == nil {
|
||
t.Fatal("selectTargetExcluding() returned nil")
|
||
}
|
||
if second.URL == first.URL {
|
||
t.Errorf("selectTargetExcluding() should not select same target %s", first.URL)
|
||
}
|
||
}
|
||
|
||
// TestSelectTargetExcluding_ConsistentHash 测试一致性哈希排除选择
|
||
func TestSelectTargetExcluding_ConsistentHash(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "consistent_hash",
|
||
HashKey: "uri",
|
||
VirtualNodes: 150,
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://backend1:8080"},
|
||
{URL: "http://backend2:8080"},
|
||
}
|
||
for _, target := range targets {
|
||
target.Healthy.Store(true)
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
|
||
// 排除一个目标,应该还能选到另一个
|
||
excluded := []*loadbalance.Target{targets[0]}
|
||
selected := p.selectTargetExcluding(ctx, excluded)
|
||
if selected == nil {
|
||
t.Error("selectTargetExcluding() should return remaining target")
|
||
}
|
||
}
|
||
|
||
// TestExtractHashKey 测试哈希键提取
|
||
func TestExtractHashKey(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
hashKey string
|
||
headers map[string]string
|
||
expected string
|
||
}{
|
||
{
|
||
name: "ip hash key",
|
||
hashKey: "ip",
|
||
headers: map[string]string{"X-Forwarded-For": "10.0.0.1"},
|
||
expected: "10.0.0.1",
|
||
},
|
||
{
|
||
name: "empty hash key defaults to ip",
|
||
hashKey: "",
|
||
headers: map[string]string{"X-Forwarded-For": "10.0.0.2"},
|
||
expected: "10.0.0.2",
|
||
},
|
||
{
|
||
name: "uri hash key",
|
||
hashKey: "uri",
|
||
headers: nil,
|
||
expected: "/api/test",
|
||
},
|
||
{
|
||
name: "header hash key - found",
|
||
hashKey: "header:X-Custom-ID",
|
||
headers: map[string]string{"X-Custom-ID": "abc123"},
|
||
expected: "abc123",
|
||
},
|
||
{
|
||
name: "header hash key - fallback to ip",
|
||
hashKey: "header:X-Missing",
|
||
headers: map[string]string{"X-Forwarded-For": "10.0.0.3"},
|
||
expected: "10.0.0.3",
|
||
},
|
||
{
|
||
name: "unknown hash key defaults to ip",
|
||
hashKey: "unknown",
|
||
headers: map[string]string{"X-Forwarded-For": "10.0.0.4"},
|
||
expected: "10.0.0.4",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "consistent_hash",
|
||
HashKey: tt.hashKey,
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtxWithHeader("GET", "/api/test", tt.headers)
|
||
result := p.extractHashKey(ctx, tt.hashKey)
|
||
if result != tt.expected {
|
||
t.Errorf("extractHashKey() = %q, want %q", result, tt.expected)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestBuildCacheKeyHash 测试缓存键哈希计算
|
||
func TestBuildCacheKeyHash(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
|
||
hashKey, origKey := p.buildCacheKeyHash(ctx)
|
||
if hashKey == 0 {
|
||
t.Error("buildCacheKeyHash() should return non-zero hash")
|
||
}
|
||
if origKey == "" {
|
||
t.Error("buildCacheKeyHash() should return non-empty origKey")
|
||
}
|
||
|
||
// 相同请求应产生相同哈希
|
||
ctx2 := testutil.NewRequestCtx("GET", "/api/test")
|
||
hashKey2, _ := p.buildCacheKeyHash(ctx2)
|
||
if hashKey != hashKey2 {
|
||
t.Error("Same request should produce same hash")
|
||
}
|
||
|
||
// 不同请求应产生不同哈希
|
||
ctx3 := testutil.NewRequestCtx("POST", "/api/other")
|
||
hashKey3, _ := p.buildCacheKeyHash(ctx3)
|
||
if hashKey == hashKey3 {
|
||
t.Error("Different request should produce different hash")
|
||
}
|
||
}
|
||
|
||
// TestBuildCacheKeyHashValue 测试零分配缓存键哈希
|
||
func TestBuildCacheKeyHashValue(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
|
||
hashValue := p.buildCacheKeyHashValue(ctx)
|
||
if hashValue == 0 {
|
||
t.Error("buildCacheKeyHashValue() should return non-zero hash")
|
||
}
|
||
|
||
// 应该与 buildCacheKeyHash 结果一致
|
||
hashKey, _ := p.buildCacheKeyHash(ctx)
|
||
if hashValue != hashKey {
|
||
t.Error("buildCacheKeyHashValue() should match buildCacheKeyHash()")
|
||
}
|
||
}
|
||
|
||
// TestWriteCachedResponse 测试缓存响应写入
|
||
func TestWriteCachedResponse(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
// 手动创建一个 Response 用于验证 writeCachedResponse 写入正确
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
|
||
entry := &cache.ProxyCacheEntry{
|
||
Data: []byte("cached body"),
|
||
Headers: map[string]string{"Content-Type": "text/html", "X-Cached": "true"},
|
||
Status: 200,
|
||
}
|
||
|
||
p.writeCachedResponse(ctx, entry)
|
||
|
||
if ctx.Response.StatusCode() != 200 {
|
||
t.Errorf("writeCachedResponse() status = %d, want 200", ctx.Response.StatusCode())
|
||
}
|
||
if string(ctx.Response.Body()) != "cached body" {
|
||
t.Errorf("writeCachedResponse() body = %q, want %q", string(ctx.Response.Body()), "cached body")
|
||
}
|
||
ct := string(ctx.Response.Header.Peek("Content-Type"))
|
||
if ct != "text/html" {
|
||
t.Errorf("writeCachedResponse() Content-Type = %q, want %q", ct, "text/html")
|
||
}
|
||
xc := string(ctx.Response.Header.Peek("X-Cache"))
|
||
if xc != "HIT" {
|
||
t.Errorf("writeCachedResponse() X-Cache = %q, want HIT", xc)
|
||
}
|
||
}
|
||
|
||
// TestGetCache 测试 GetCache 方法
|
||
func TestGetCache(t *testing.T) {
|
||
// 启用缓存时
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
Cache: config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: 10 * time.Second,
|
||
},
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
c := p.GetCache()
|
||
if c == nil {
|
||
t.Error("GetCache() should return non-nil when cache enabled")
|
||
}
|
||
|
||
// 禁用缓存时
|
||
cfg2 := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
p2, _ := NewProxy(cfg2, targets, nil, nil)
|
||
c2 := p2.GetCache()
|
||
if c2 != nil {
|
||
t.Error("GetCache() should return nil when cache disabled")
|
||
}
|
||
}
|
||
|
||
// TestGetCacheStats 测试 GetCacheStats 方法
|
||
func TestGetCacheStats(t *testing.T) {
|
||
// 启用缓存时
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
Cache: config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: 10 * time.Second,
|
||
},
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
stats := p.GetCacheStats()
|
||
if stats == nil {
|
||
t.Error("GetCacheStats() should return non-nil when cache enabled")
|
||
}
|
||
|
||
// 禁用缓存时
|
||
cfg2 := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
p2, _ := NewProxy(cfg2, targets, nil, nil)
|
||
stats2 := p2.GetCacheStats()
|
||
if stats2 != nil {
|
||
t.Error("GetCacheStats() should return nil when cache disabled")
|
||
}
|
||
}
|
||
|
||
// TestGetCacheDuration 测试不同状态码的缓存时间计算
|
||
func TestGetCacheDuration(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
cacheValid *config.ProxyCacheValidConfig
|
||
maxAge time.Duration
|
||
statusCode int
|
||
expected time.Duration
|
||
}{
|
||
{
|
||
name: "no CacheValid config uses MaxAge",
|
||
maxAge: 5 * time.Minute,
|
||
statusCode: 200,
|
||
expected: 5 * time.Minute,
|
||
},
|
||
{
|
||
name: "2xx with CacheValid.OK set",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
OK: 10 * time.Minute,
|
||
},
|
||
statusCode: 200,
|
||
expected: 10 * time.Minute,
|
||
},
|
||
{
|
||
name: "2xx with CacheValid.OK=0 inherits MaxAge",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
OK: 0,
|
||
},
|
||
maxAge: 3 * time.Minute,
|
||
statusCode: 201,
|
||
expected: 3 * time.Minute,
|
||
},
|
||
{
|
||
name: "301 redirect",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
Redirect: 1 * time.Hour,
|
||
},
|
||
statusCode: 301,
|
||
expected: 1 * time.Hour,
|
||
},
|
||
{
|
||
name: "302 redirect",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
Redirect: 30 * time.Minute,
|
||
},
|
||
statusCode: 302,
|
||
expected: 30 * time.Minute,
|
||
},
|
||
{
|
||
name: "302 with zero Redirect means no cache",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
Redirect: 0,
|
||
},
|
||
statusCode: 302,
|
||
expected: 0,
|
||
},
|
||
{
|
||
name: "404",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
NotFound: 1 * time.Minute,
|
||
},
|
||
statusCode: 404,
|
||
expected: 1 * time.Minute,
|
||
},
|
||
{
|
||
name: "4xx client error",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
ClientError: 30 * time.Second,
|
||
},
|
||
statusCode: 400,
|
||
expected: 30 * time.Second,
|
||
},
|
||
{
|
||
name: "5xx server error",
|
||
cacheValid: &config.ProxyCacheValidConfig{
|
||
ServerError: 0,
|
||
},
|
||
statusCode: 500,
|
||
expected: 0,
|
||
},
|
||
{
|
||
name: "other status code",
|
||
statusCode: 100,
|
||
expected: 0,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
Cache: config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: tt.maxAge,
|
||
},
|
||
CacheValid: tt.cacheValid,
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
duration := p.getCacheDuration(tt.statusCode)
|
||
if duration != tt.expected {
|
||
t.Errorf("getCacheDuration(%d) = %v, want %v", tt.statusCode, duration, tt.expected)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestBackgroundRefresh 测试后台缓存刷新(标记为 skip 因为需要真实网络)
|
||
func TestBackgroundRefresh(t *testing.T) {
|
||
t.Skip("skipping: requires real network connection and is timing-sensitive")
|
||
ln := fasthttputil.NewInmemoryListener()
|
||
defer func() { _ = ln.Close() }()
|
||
|
||
go func() {
|
||
s := &fasthttp.Server{
|
||
Handler: func(ctx *fasthttp.RequestCtx) {
|
||
ctx.SetStatusCode(200)
|
||
ctx.SetBodyString("refreshed")
|
||
ctx.Response.Header.Set("Content-Type", "text/plain")
|
||
},
|
||
}
|
||
_ = s.Serve(ln)
|
||
}()
|
||
time.Sleep(10 * time.Millisecond)
|
||
|
||
addr := ln.Addr().String()
|
||
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
Cache: config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: 10 * time.Second,
|
||
},
|
||
}
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://" + addr},
|
||
}
|
||
targets[0].Healthy.Store(true)
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
|
||
// 设置缓存键
|
||
hashKey := uint64(12345)
|
||
|
||
// 调用后台刷新(它会执行实际请求来刷新缓存)
|
||
done := make(chan struct{})
|
||
go func() {
|
||
p.backgroundRefresh(&ctx.Request, targets[0], hashKey, "/api/test")
|
||
close(done)
|
||
}()
|
||
|
||
// 等待完成
|
||
select {
|
||
case <-done:
|
||
// 完成
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("backgroundRefresh() timed out")
|
||
}
|
||
}
|
||
|
||
// TestBackgroundRefresh_NoClient 测试后台刷新时客户端不存在的情况
|
||
func TestBackgroundRefresh_NoClient(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
Cache: config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: 10 * time.Second,
|
||
},
|
||
}
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://nonexistent:9999"},
|
||
}
|
||
targets[0].Healthy.Store(true)
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
// 移除客户端
|
||
p.mu.Lock()
|
||
delete(p.clients, targets[0].URL)
|
||
p.mu.Unlock()
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
hashKey := uint64(99999)
|
||
|
||
// 应该不会 panic,直接返回
|
||
p.backgroundRefresh(&ctx.Request, targets[0], hashKey, "/api/test")
|
||
}
|
||
|
||
// TestServeHTTP_CacheHit 测试缓存命中路径
|
||
func TestServeHTTP_CacheHit(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
Cache: config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: 10 * time.Second,
|
||
},
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
targets[0].Healthy.Store(true)
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
// 预填充缓存
|
||
ctx := testutil.NewRequestCtx("GET", "/api/cached")
|
||
hashKey, origKey := p.buildCacheKeyHash(ctx)
|
||
p.cache.Set(hashKey, origKey, []byte("cached!"), map[string]string{
|
||
"Content-Type": "text/plain",
|
||
}, 200, 10*time.Second)
|
||
|
||
// 执行请求
|
||
p.ServeHTTP(ctx)
|
||
|
||
// 应该返回缓存的响应
|
||
if ctx.Response.StatusCode() != 200 {
|
||
t.Errorf("ServeHTTP() status = %d, want 200", ctx.Response.StatusCode())
|
||
}
|
||
if string(ctx.Response.Body()) != "cached!" {
|
||
t.Errorf("ServeHTTP() body = %q, want %q", string(ctx.Response.Body()), "cached!")
|
||
}
|
||
xc := string(ctx.Response.Header.Peek("X-Cache"))
|
||
if xc != "HIT" {
|
||
t.Errorf("ServeHTTP() X-Cache = %q, want HIT", xc)
|
||
}
|
||
}
|
||
|
||
// TestServeHTTP_ClientNil 测试客户端为 nil 时的行为
|
||
func TestServeHTTP_ClientNil(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
NextUpstream: config.NextUpstreamConfig{
|
||
Tries: 2,
|
||
},
|
||
}
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://backend1:8080"},
|
||
{URL: "http://backend2:8080"},
|
||
}
|
||
for _, target := range targets {
|
||
target.Healthy.Store(true)
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
// 移除所有客户端
|
||
p.mu.Lock()
|
||
p.clients = make(map[string]*fasthttp.HostClient)
|
||
p.mu.Unlock()
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
p.ServeHTTP(ctx)
|
||
|
||
// 所有客户端都不存在,应该返回 502
|
||
if ctx.Response.StatusCode() != fasthttp.StatusBadGateway {
|
||
t.Errorf("ServeHTTP() status = %d, want 502", ctx.Response.StatusCode())
|
||
}
|
||
}
|
||
|
||
// TestServeHTTP_WithRedirectRewrite 测试带 redirect_rewrite 的缓存命中
|
||
func TestServeHTTP_WithRedirectRewrite_CacheHit(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
Cache: config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: 10 * time.Second,
|
||
},
|
||
RedirectRewrite: &config.RedirectRewriteConfig{
|
||
Mode: "off", // 关闭改写
|
||
},
|
||
}
|
||
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
|
||
targets[0].Healthy.Store(true)
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
ctx := testutil.NewRequestCtx("GET", "/api/test")
|
||
hashKey, origKey := p.buildCacheKeyHash(ctx)
|
||
p.cache.Set(hashKey, origKey, []byte("ok"), map[string]string{
|
||
"Content-Type": "text/plain",
|
||
}, 200, 10*time.Second)
|
||
|
||
p.ServeHTTP(ctx)
|
||
|
||
if ctx.Response.StatusCode() != 200 {
|
||
t.Errorf("ServeHTTP() status = %d, want 200", ctx.Response.StatusCode())
|
||
}
|
||
}
|