将 parseSize 的 if-else 改为 switch 语句;更新集成测试中 NewProxyCache 调用以匹配新增的 stale_if_error/stale_if_timeout 参数。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
276 lines
6.7 KiB
Go
276 lines
6.7 KiB
Go
//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, 0, 0)
|
||
require.NotNil(t, pc)
|
||
}
|
||
|
||
// TestProxyCacheDisabled 测试禁用缓存(空规则)
|
||
func TestProxyCacheDisabled(t *testing.T) {
|
||
rules := []cache.ProxyCacheRule{}
|
||
|
||
pc := cache.NewProxyCache(rules, false, 0, 0, 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, 0, 0)
|
||
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, 0, 0)
|
||
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, 0, 0)
|
||
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, 0, 0)
|
||
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, 0, 0)
|
||
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, 0, 0)
|
||
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, 0, 0)
|
||
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 计数应该是 6(5次循环 + 1次验证)
|
||
assert.GreaterOrEqual(t, entry.Uses.Load(), int32(6))
|
||
}
|