test(integration): 添加缓存和代理集成测试

- cache_integration_test.go: 缓存创建、存取、过期、stale、并发测试
- proxy_integration_test.go: 代理创建、负载均衡、超时、健康检查测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-22 14:59:12 +08:00
parent ad88770472
commit 604bc719f9
2 changed files with 679 additions and 0 deletions

View File

@ -0,0 +1,275 @@
//go:build integration
// cache_integration_test.go - 缓存集成测试L2 层,进程内)
//
// 测试代理缓存的核心功能。
//
// 作者xfy
package integration
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"rua.plus/lolly/internal/cache"
)
// TestProxyCacheCreation 测试代理缓存创建
func TestProxyCacheCreation(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/api/",
Methods: []string{"GET", "HEAD"},
Statuses: []int{200, 301, 302},
MaxAge: 60 * time.Second,
},
}
pc := cache.NewProxyCache(rules, true, 30*time.Second)
require.NotNil(t, pc)
}
// TestProxyCacheDisabled 测试禁用缓存(空规则)
func TestProxyCacheDisabled(t *testing.T) {
rules := []cache.ProxyCacheRule{}
pc := cache.NewProxyCache(rules, false, 0)
require.NotNil(t, pc)
}
// TestProxyCacheSetAndGet 测试缓存存取
func TestProxyCacheSetAndGet(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/",
Methods: []string{"GET"},
Statuses: []int{200},
MaxAge: 60 * time.Second,
},
}
pc := cache.NewProxyCache(rules, true, 30*time.Second)
require.NotNil(t, pc)
origKey := "GET:/test"
hashKey := cache.HashPathWithMethod("/test", "GET")
body := []byte("test response body")
headers := map[string]string{
"Content-Type": "application/json",
"X-Custom": "value",
}
// 存入缓存
pc.Set(hashKey, origKey, body, headers, 200, 60*time.Second)
// 从缓存获取
entry, found, stale := pc.Get(hashKey, origKey)
assert.True(t, found)
assert.False(t, stale)
require.NotNil(t, entry)
assert.Equal(t, 200, entry.Status)
assert.Equal(t, body, entry.Data)
assert.Equal(t, "application/json", entry.Headers["Content-Type"])
}
// TestProxyCacheMiss 测试缓存未命中
func TestProxyCacheMiss(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/",
Methods: []string{"GET"},
Statuses: []int{200},
MaxAge: 60 * time.Second,
},
}
pc := cache.NewProxyCache(rules, true, 30*time.Second)
require.NotNil(t, pc)
origKey := "GET:/nonexistent"
hashKey := cache.HashPathWithMethod("/nonexistent", "GET")
// 不存在的键应返回未命中
entry, found, stale := pc.Get(hashKey, origKey)
assert.False(t, found)
assert.False(t, stale)
assert.Nil(t, entry)
}
// TestProxyCacheExpiration 测试缓存过期
func TestProxyCacheExpiration(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/",
Methods: []string{"GET"},
Statuses: []int{200},
MaxAge: 100 * time.Millisecond, // 短过期时间
},
}
pc := cache.NewProxyCache(rules, true, 50*time.Millisecond)
require.NotNil(t, pc)
origKey := "GET:/expiring"
hashKey := cache.HashPathWithMethod("/expiring", "GET")
body := []byte("will expire")
// 存入缓存
pc.Set(hashKey, origKey, body, nil, 200, 100*time.Millisecond)
// 立即获取应该命中
entry, found, _ := pc.Get(hashKey, origKey)
assert.True(t, found)
assert.NotNil(t, entry)
// 等待过期
time.Sleep(150 * time.Millisecond)
// 过期后应该未命中
entry, found, stale := pc.Get(hashKey, origKey)
assert.False(t, found)
assert.False(t, stale)
assert.Nil(t, entry)
}
// TestProxyCacheStale 测试过期缓存复用
func TestProxyCacheStale(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/",
Methods: []string{"GET"},
Statuses: []int{200},
MaxAge: 100 * time.Millisecond,
},
}
// staleTime = 200ms允许过期后 200ms 内复用
pc := cache.NewProxyCache(rules, true, 200*time.Millisecond)
require.NotNil(t, pc)
origKey := "GET:/stale"
hashKey := cache.HashPathWithMethod("/stale", "GET")
body := []byte("stale data")
// 存入缓存
pc.Set(hashKey, origKey, body, nil, 200, 100*time.Millisecond)
// 等待过期但仍在 stale 时间内
time.Sleep(150 * time.Millisecond)
// 应该返回 stale 数据
entry, found, stale := pc.Get(hashKey, origKey)
assert.True(t, found)
assert.True(t, stale)
assert.NotNil(t, entry)
assert.Equal(t, body, entry.Data)
}
// TestProxyCacheHashKeyCollision 测试哈希碰撞检测
func TestProxyCacheHashKeyCollision(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/",
Methods: []string{"GET"},
Statuses: []int{200},
MaxAge: 60 * time.Second,
},
}
pc := cache.NewProxyCache(rules, true, 30*time.Second)
require.NotNil(t, pc)
// 两个不同的 key
key1 := "GET:/resource/1"
key2 := "GET:/resource/2"
hashKey1 := cache.HashPathWithMethod("/resource/1", "GET")
hashKey2 := cache.HashPathWithMethod("/resource/2", "GET")
// 存入不同的数据
pc.Set(hashKey1, key1, []byte("data1"), nil, 200, 60*time.Second)
pc.Set(hashKey2, key2, []byte("data2"), nil, 200, 60*time.Second)
// 验证各自返回正确的数据
entry1, found1, _ := pc.Get(hashKey1, key1)
assert.True(t, found1)
assert.Equal(t, []byte("data1"), entry1.Data)
entry2, found2, _ := pc.Get(hashKey2, key2)
assert.True(t, found2)
assert.Equal(t, []byte("data2"), entry2.Data)
}
// TestProxyCacheConcurrent 测试并发访问
func TestProxyCacheConcurrent(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/",
Methods: []string{"GET"},
Statuses: []int{200},
MaxAge: 60 * time.Second,
},
}
pc := cache.NewProxyCache(rules, true, 30*time.Second)
require.NotNil(t, pc)
// 并发写入
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(idx int) {
key := "GET:/concurrent"
hashKey := cache.HashPathWithMethod("/concurrent", "GET")
pc.Set(hashKey, key, []byte("concurrent data"), nil, 200, 60*time.Second)
done <- true
}(i)
}
// 等待所有写入完成
for i := 0; i < 10; i++ {
<-done
}
// 验证数据一致性
key := "GET:/concurrent"
hashKey := cache.HashPathWithMethod("/concurrent", "GET")
entry, found, _ := pc.Get(hashKey, key)
assert.True(t, found)
assert.NotNil(t, entry)
}
// TestProxyCacheUsesCount 测试访问计数
func TestProxyCacheUsesCount(t *testing.T) {
rules := []cache.ProxyCacheRule{
{
Path: "/",
Methods: []string{"GET"},
Statuses: []int{200},
MaxAge: 60 * time.Second,
},
}
pc := cache.NewProxyCache(rules, true, 30*time.Second)
require.NotNil(t, pc)
origKey := "GET:/counted"
hashKey := cache.HashPathWithMethod("/counted", "GET")
// 存入缓存
pc.Set(hashKey, origKey, []byte("data"), nil, 200, 60*time.Second)
// 多次访问
for i := 0; i < 5; i++ {
entry, found, _ := pc.Get(hashKey, origKey)
assert.True(t, found)
require.NotNil(t, entry)
}
// 验证访问计数
entry, _, _ := pc.Get(hashKey, origKey)
require.NotNil(t, entry)
// Uses 计数应该是 65次循环 + 1次验证
assert.GreaterOrEqual(t, entry.Uses.Load(), int32(6))
}

View File

@ -0,0 +1,404 @@
//go:build integration
// proxy_integration_test.go - 代理集成测试L2 层,进程内)
//
// 测试反向代理的配置和创建逻辑。
// 实际的网络转发测试在 L3 E2E 测试中进行。
//
// 作者xfy
package integration
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/loadbalance"
"rua.plus/lolly/internal/proxy"
)
// TestProxyCreation 测试代理创建
func TestProxyCreation(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
Write: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{
URL: "http://127.0.0.1:8081",
Weight: 1,
MaxFails: 3,
},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err, "failed to create proxy")
require.NotNil(t, p)
// 验证配置
assert.Equal(t, "round_robin", cfg.LoadBalance)
assert.Equal(t, 5*time.Second, cfg.Timeout.Connect)
assert.Equal(t, 10*time.Second, cfg.Timeout.Read)
}
// TestProxyRequestHeaders 测试请求头修改配置
func TestProxyRequestHeaders(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
Headers: config.ProxyHeaders{
SetRequest: map[string]string{
"X-Custom-Header": "custom-value",
"X-Forwarded-Proto": "https",
},
},
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证代理配置已设置
assert.NotNil(t, cfg.Headers.SetRequest)
assert.Equal(t, "custom-value", cfg.Headers.SetRequest["X-Custom-Header"])
assert.Equal(t, "https", cfg.Headers.SetRequest["X-Forwarded-Proto"])
}
// TestProxyResponseHeaders 测试响应头修改配置
func TestProxyResponseHeaders(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
Headers: config.ProxyHeaders{
SetResponse: map[string]string{
"X-Server": "lolly",
},
Remove: []string{"X-Powered-By"},
},
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证响应头配置
assert.Equal(t, "lolly", cfg.Headers.SetResponse["X-Server"])
assert.Contains(t, cfg.Headers.Remove, "X-Powered-By")
}
// TestProxyTimeout 测试代理超时配置
func TestProxyTimeout(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 1 * time.Second,
Read: 50 * time.Millisecond,
Write: 1 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证超时配置
assert.Equal(t, 1*time.Second, cfg.Timeout.Connect)
assert.Equal(t, 50*time.Millisecond, cfg.Timeout.Read)
assert.Equal(t, 1*time.Second, cfg.Timeout.Write)
}
// TestProxyLoadBalanceRoundRobin 测试轮询负载均衡配置
func TestProxyLoadBalanceRoundRobin(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
{URL: "http://127.0.0.1:8082", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证负载均衡器类型
assert.Equal(t, "round_robin", cfg.LoadBalance)
assert.Len(t, targets, 2)
}
// TestProxyWeightedRoundRobin 测试加权轮询配置
func TestProxyWeightedRoundRobin(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "weighted_round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 3},
{URL: "http://127.0.0.1:8082", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证权重配置
assert.Equal(t, 3, targets[0].Weight)
assert.Equal(t, 1, targets[1].Weight)
}
// TestProxyLeastConn 测试最少连接负载均衡配置
func TestProxyLeastConn(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "least_conn",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
{URL: "http://127.0.0.1:8082", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
assert.Equal(t, "least_conn", cfg.LoadBalance)
}
// TestProxyIPHash 测试 IP 哈希负载均衡配置
func TestProxyIPHash(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "ip_hash",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
{URL: "http://127.0.0.1:8082", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
assert.Equal(t, "ip_hash", cfg.LoadBalance)
}
// TestProxyConsistentHash 测试一致性哈希负载均衡配置
func TestProxyConsistentHash(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "consistent_hash",
HashKey: "uri",
VirtualNodes: 150,
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
{URL: "http://127.0.0.1:8082", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
assert.Equal(t, "consistent_hash", cfg.LoadBalance)
assert.Equal(t, "uri", cfg.HashKey)
assert.Equal(t, 150, cfg.VirtualNodes)
}
// TestProxyErrorHandling 测试错误处理配置
func TestProxyErrorHandling(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{
URL: "http://127.0.0.1:8081",
Weight: 1,
MaxFails: 3,
FailTimeout: 10 * time.Second,
},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证 MaxFails 配置 (int64 类型)
assert.Equal(t, int64(3), targets[0].MaxFails)
assert.Equal(t, 10*time.Second, targets[0].FailTimeout)
}
// TestProxyCacheConfig 测试缓存配置
func TestProxyCacheConfig(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
Cache: config.ProxyCacheConfig{
Enabled: true,
MaxAge: 60 * time.Second,
Methods: []string{"GET", "HEAD"},
MinUses: 1,
CacheLock: true,
CacheLockTimeout: 5 * time.Second,
StaleWhileRevalidate: 30 * time.Second,
},
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证缓存配置
assert.True(t, cfg.Cache.Enabled)
assert.Equal(t, 60*time.Second, cfg.Cache.MaxAge)
assert.Contains(t, cfg.Cache.Methods, "GET")
assert.True(t, cfg.Cache.CacheLock)
}
// TestProxyNextUpstream 测试故障转移配置
func TestProxyNextUpstream(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
NextUpstream: config.NextUpstreamConfig{
Tries: 3,
HTTPCodes: []int{502, 503, 504},
},
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
{URL: "http://127.0.0.1:8082", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证故障转移配置
assert.Equal(t, 3, cfg.NextUpstream.Tries)
assert.Contains(t, cfg.NextUpstream.HTTPCodes, 502)
assert.Contains(t, cfg.NextUpstream.HTTPCodes, 503)
assert.Contains(t, cfg.NextUpstream.HTTPCodes, 504)
}
// TestProxyHealthCheck 测试健康检查配置
func TestProxyHealthCheck(t *testing.T) {
cfg := &config.ProxyConfig{
LoadBalance: "round_robin",
HealthCheck: config.HealthCheckConfig{
Interval: 10 * time.Second,
Path: "/health",
Timeout: 5 * time.Second,
},
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 10 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://127.0.0.1:8081", Weight: 1},
}
p, err := proxy.NewProxy(cfg, targets, nil, nil)
require.NoError(t, err)
err = p.Start()
require.NoError(t, err)
defer p.Stop()
// 验证健康检查配置
assert.Equal(t, 10*time.Second, cfg.HealthCheck.Interval)
assert.Equal(t, "/health", cfg.HealthCheck.Path)
assert.Equal(t, 5*time.Second, cfg.HealthCheck.Timeout)
}