lolly/internal/cache/cache_test.go

621 lines
17 KiB
Go
Raw Permalink 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 cache 提供缓存功能的测试。
//
// 该文件测试缓存模块的各项功能,包括:
// - 文件缓存创建和配置
// - 代理缓存规则和匹配
// - 缓存设置和获取
// - 过期和淘汰策略
// - 路径匹配功能
//
// 作者xfy
package cache
import (
"hash/fnv"
"testing"
"time"
)
// hashKey 计算字符串的 FNV-64a 哈希值,用于测试。
func hashKey(s string) uint64 {
h := fnv.New64a()
h.Write([]byte(s))
return h.Sum64()
}
func TestNewFileCache(t *testing.T) {
fc := NewFileCache(100, 1024*1024, 30*time.Second)
if fc == nil {
t.Error("Expected non-nil FileCache")
}
}
func TestFileCacheSetGet(t *testing.T) {
fc := NewFileCache(10, 1024, 1*time.Hour)
path := "/test/file.txt"
data := []byte("Hello, World!")
err := fc.Set(path, data, int64(len(data)), time.Now(), "text/plain")
if err != nil {
t.Errorf("Set() error: %v", err)
}
entry, ok := fc.Get(path)
if !ok {
t.Error("Expected to find cached entry")
}
if string(entry.Data) != "Hello, World!" {
t.Errorf("Expected data 'Hello, World!', got %s", entry.Data)
}
}
func TestFileCacheDelete(t *testing.T) {
fc := NewFileCache(10, 1024, 1*time.Hour)
_ = fc.Set("/test.txt", []byte("data"), 4, time.Now(), "text/plain")
fc.Delete("/test.txt")
_, ok := fc.Get("/test.txt")
if ok {
t.Error("Expected entry to be deleted")
}
}
func TestFileCacheLRUEviction(t *testing.T) {
// 最大 3 个条目
fc := NewFileCache(3, 0, 1*time.Hour)
_ = fc.Set("/a", []byte("a"), 1, time.Now(), "text/plain")
_ = fc.Set("/b", []byte("b"), 1, time.Now(), "text/plain")
_ = fc.Set("/c", []byte("c"), 1, time.Now(), "text/plain")
// 再添加一个,应该淘汰 /a
_ = fc.Set("/d", []byte("d"), 1, time.Now(), "text/plain")
_, ok := fc.Get("/a")
if ok {
t.Error("Expected /a to be evicted")
}
// b, c, d 应该还在
for _, path := range []string{"b", "c", "d"} {
_, ok := fc.Get("/" + path)
if !ok {
t.Errorf("Expected /%s to exist", path)
}
}
}
func TestAcquireLockWithTimeout(t *testing.T) {
pc := NewProxyCache(nil, true, 0, 0, 0)
key := hashKey("timeout-test")
// 测试获取锁
waitCh, timedOut := pc.AcquireLockWithTimeout(key, 100*time.Millisecond)
if waitCh != nil || timedOut {
t.Error("Expected to acquire lock immediately")
}
// 测试等待超时
done := make(chan struct{})
go func() {
time.Sleep(200 * time.Millisecond)
pc.ReleaseLock(key, nil)
close(done)
}()
_, timedOut = pc.AcquireLockWithTimeout(key, 50*time.Millisecond)
if !timedOut {
t.Error("Expected timeout when waiting for lock")
}
<-done
}
func TestRefreshTTL(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
key := hashKey("refresh-test")
origKey := "refresh-test"
pc.Set(key, origKey, []byte("data"), nil, 200, 10*time.Minute)
newHeaders := map[string]string{
"Last-Modified": "Wed, 21 Oct 2015 07:28:00 GMT",
"ETag": "\"abc123\"",
}
ok := pc.RefreshTTL(key, origKey, newHeaders)
if !ok {
t.Error("Expected RefreshTTL to succeed")
}
entry, _, _ := pc.Get(key, origKey)
if entry.LastModified != newHeaders["Last-Modified"] {
t.Errorf("Expected Last-Modified to be updated")
}
if entry.ETag != newHeaders["ETag"] {
t.Errorf("Expected ETag to be updated")
}
}
func TestSetValidationHeaders(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
key := hashKey("validation-test")
origKey := "validation-test"
pc.Set(key, origKey, []byte("data"), nil, 200, 10*time.Minute)
ok := pc.SetValidationHeaders(key, origKey, "Mon, 01 Jan 2024 00:00:00 GMT", "\"xyz789\"")
if !ok {
t.Error("Expected SetValidationHeaders to succeed")
}
entry, _, _ := pc.Get(key, origKey)
if entry.LastModified != "Mon, 01 Jan 2024 00:00:00 GMT" {
t.Errorf("Expected LastModified to be set")
}
if entry.ETag != "\"xyz789\"" {
t.Errorf("Expected ETag to be set")
}
}
func TestMatchRulePathVariants(t *testing.T) {
tests := []struct {
name string
rulePath string
reqPath string
want bool
}{
{"prefix_match", "/api/", "/api/users", true},
{"prefix_no_match", "/api/", "/other", false},
{"wildcard_match", "/static/*", "/static/css/style.css", true},
{"wildcard_no_match", "/static/*", "/api/users", false},
{"exact_match", "/api", "/api", true},
{"prefix_with_slash", "/api", "/api/users", true},
{"prefix_with_query", "/api", "/api?query=value", true},
{"no_match_similar", "/api", "/apiother", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rules := []ProxyCacheRule{
{Path: tt.rulePath, Methods: []string{"GET"}, MaxAge: time.Minute},
}
pc := NewProxyCache(rules, false, 0, 0, 0)
rule := pc.MatchRule(tt.reqPath, "GET", 0)
if (rule != nil) != tt.want {
t.Errorf("MatchRule(%s, %s) = %v, want %v", tt.rulePath, tt.reqPath, rule != nil, tt.want)
}
})
}
}
func TestMinUsesThreshold(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
key := hashKey("minuses-test")
origKey := "minuses-test"
// 首次设置
pc.Set(key, origKey, []byte("data"), nil, 200, 10*time.Minute)
// 首次 GetUses = 1
entry, _, _ := pc.Get(key, origKey)
if entry.Uses.Load() != 1 {
t.Errorf("Expected Uses=1 after first Get, got %d", entry.Uses.Load())
}
// 第二次 GetUses = 2
pc.Get(key, origKey)
if entry.Uses.Load() != 2 {
t.Errorf("Expected Uses=2 after second Get, got %d", entry.Uses.Load())
}
}
func TestFileCacheSizeEviction(t *testing.T) {
// 最大 10 字节
fc := NewFileCache(0, 10, 1*time.Hour)
_ = fc.Set("/a", []byte("12345"), 5, time.Now(), "text/plain")
_ = fc.Set("/b", []byte("12345"), 5, time.Now(), "text/plain")
// 再添加 6 字节,应该淘汰一个
_ = fc.Set("/c", []byte("123456"), 6, time.Now(), "text/plain")
stats := fc.Stats()
if stats.Size > 10 {
t.Errorf("Expected size <= 10, got %d", stats.Size)
}
}
func TestFileCacheInactiveEviction(t *testing.T) {
fc := NewFileCache(10, 1024, 100*time.Millisecond)
_ = fc.Set("/test", []byte("data"), 4, time.Now(), "text/plain")
// 立即获取应该成功
_, ok := fc.Get("/test")
if !ok {
t.Error("Expected entry to exist")
}
// 等待过期
time.Sleep(150 * time.Millisecond)
// 再次获取应该失败(因过期被删除)
_, ok = fc.Get("/test")
if ok {
t.Error("Expected entry to be expired")
}
}
func TestFileCacheStats(t *testing.T) {
fc := NewFileCache(100, 1024, 1*time.Hour)
_ = fc.Set("/a", []byte("12345"), 5, time.Now(), "text/plain")
_ = fc.Set("/b", []byte("12345"), 5, time.Now(), "text/plain")
stats := fc.Stats()
if stats.Entries != 2 {
t.Errorf("Expected 2 entries, got %d", stats.Entries)
}
if stats.Size != 10 {
t.Errorf("Expected size 10, got %d", stats.Size)
}
}
func TestNewProxyCache(t *testing.T) {
rules := []ProxyCacheRule{
{Path: "/api/", Methods: []string{"GET"}, MaxAge: 10 * time.Minute},
}
pc := NewProxyCache(rules, true, 60*time.Second, 0, 0)
if pc == nil {
t.Error("Expected non-nil ProxyCache")
}
}
func TestProxyCacheSetGet(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
key := "test-key"
data := []byte("response body")
headers := map[string]string{"Content-Type": "application/json"}
pc.Set(hashKey(key), key, data, headers, 200, 10*time.Minute)
entry, ok, stale := pc.Get(hashKey(key), key)
if !ok {
t.Error("Expected to find cached entry")
}
if stale {
t.Error("Expected entry to be fresh")
}
if string(entry.Data) != "response body" {
t.Errorf("Expected data 'response body', got %s", entry.Data)
}
if entry.Status != 200 {
t.Errorf("Expected status 200, got %d", entry.Status)
}
}
func TestProxyCacheExpiration(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
key := "expire-test"
pc.Set(hashKey(key), key, []byte("data"), nil, 200, 100*time.Millisecond)
// 立即获取应该成功
_, ok, _ := pc.Get(hashKey(key), key)
if !ok {
t.Error("Expected entry to exist")
}
// 等待过期
time.Sleep(150 * time.Millisecond)
_, ok, _ = pc.Get(hashKey(key), key)
if ok {
t.Error("Expected entry to be expired")
}
}
func TestProxyCacheStaleWhileRevalidate(t *testing.T) {
pc := NewProxyCache(nil, false, 200*time.Millisecond, 0, 0)
key := "stale-test"
pc.Set(hashKey(key), key, []byte("data"), nil, 200, 100*time.Millisecond)
// 等待过期但仍在 stale 时间内
time.Sleep(150 * time.Millisecond)
entry, ok, stale := pc.Get(hashKey(key), key)
if !ok {
t.Error("Expected stale entry to be usable")
}
if !stale {
t.Error("Expected entry to be marked as stale")
}
if entry == nil {
t.Error("Expected stale entry data")
}
}
func TestProxyCacheMatchRule(t *testing.T) {
rules := []ProxyCacheRule{
{Path: "/api/", Methods: []string{"GET"}, Statuses: []int{200}, MaxAge: 10 * time.Minute},
{Path: "/static/*", Methods: []string{"GET"}, MaxAge: 1 * time.Hour},
}
pc := NewProxyCache(rules, false, 0, 0, 0)
tests := []struct {
path string
method string
status int
want bool
}{
{"api/users", "GET", 200, true},
{"api/users", "POST", 200, false}, // POST 不在 Methods
{"api/users", "GET", 404, false}, // 404 不在 Statuses
{"static/css/style.css", "GET", 200, true},
{"other/path", "GET", 200, false}, // 不匹配任何规则
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
// 添加前缀 / 到 path
fullPath := "/" + tt.path
rule := pc.MatchRule(fullPath, tt.method, tt.status)
if (rule != nil) != tt.want {
t.Errorf("MatchRule(%s, %s, %d) want %v", fullPath, tt.method, tt.status, tt.want)
}
})
}
}
func TestProxyCacheDelete(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
key := "key1"
pc.Set(hashKey(key), key, []byte("data"), nil, 200, 10*time.Minute)
pc.Delete(hashKey(key))
_, ok, _ := pc.Get(hashKey(key), key)
if ok {
t.Error("Expected entry to be deleted")
}
}
func TestPathMatch(t *testing.T) {
tests := []struct {
pattern string
path string
want bool
}{
{"*", "/anything", true},
{"api/*", "/api/users", true},
{"api/*", "/api/", true},
{"api/*", "/other", false},
{"/exact", "/exact", true},
{"/exact", "/exact/other", false},
}
for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.path, func(t *testing.T) {
// 添加前缀 / 如果 pattern 没有
pattern := tt.pattern
if pattern[0] != '/' && pattern != "*" {
pattern = "/" + pattern
}
result := MatchPattern(pattern, tt.path)
if result != tt.want {
t.Errorf("MatchPattern(%s, %s) = %v, want %v", pattern, tt.path, result, tt.want)
}
})
}
}
func TestProxyCacheGetStaleIfError(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 200*time.Millisecond, 0)
key := "stale-error-test"
pc.Set(hashKey(key), key, []byte("data"), nil, 200, 100*time.Millisecond)
// 等待过期但仍在 stale_if_error 窗口内
time.Sleep(150 * time.Millisecond)
// isTimeout=false应该使用 staleIfError 窗口
entry, ok := pc.GetStale(hashKey(key), key, false)
if !ok {
t.Error("stale entry should be usable on error")
}
if entry == nil {
t.Error("expected stale entry data")
}
if string(entry.Data) != "data" {
t.Errorf("entry.Data = %q, want %q", entry.Data, "data")
}
// isTimeout=truestaleIfTimeout=0不应该可用
if _, ok2 := pc.GetStale(hashKey(key), key, true); ok2 {
t.Error("stale entry should NOT be usable on timeout when staleIfTimeout=0")
}
}
func TestProxyCacheGetStaleIfTimeout(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 300*time.Millisecond)
key := "stale-timeout-test"
pc.Set(hashKey(key), key, []byte("data"), nil, 200, 100*time.Millisecond)
// 等待过期但仍在 stale_if_timeout 窗口内
time.Sleep(250 * time.Millisecond)
// isTimeout=true应该使用 staleIfTimeout 窗口
entry, ok := pc.GetStale(hashKey(key), key, true)
if !ok {
t.Error("stale entry should be usable on timeout")
}
if entry == nil {
t.Error("expected stale entry data")
}
// isTimeout=falsestaleIfError=0不应该可用
if _, ok2 := pc.GetStale(hashKey(key), key, false); ok2 {
t.Error("stale entry should NOT be usable on error when staleIfError=0")
}
}
func TestProxyCacheGetStaleExpired(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 100*time.Millisecond, 100*time.Millisecond)
key := "stale-expired-test"
pc.Set(hashKey(key), key, []byte("data"), nil, 200, 50*time.Millisecond)
// 等待超过 stale 窗口
time.Sleep(200 * time.Millisecond)
// 两种情况都不应该可用
if _, ok := pc.GetStale(hashKey(key), key, false); ok {
t.Error("stale entry should NOT be usable after stale window expired")
}
if _, ok2 := pc.GetStale(hashKey(key), key, true); ok2 {
t.Error("stale entry should NOT be usable on timeout after stale window expired")
}
}
func TestProxyCacheGetStaleNotExpired(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 100*time.Millisecond, 100*time.Millisecond)
key := "stale-fresh-test"
pc.Set(hashKey(key), key, []byte("data"), nil, 200, 200*time.Millisecond)
// 未过期,两种情况都应该可用(返回新鲜数据)
entry, ok := pc.GetStale(hashKey(key), key, false)
if !ok {
t.Error("fresh entry should be usable")
}
if string(entry.Data) != "data" {
t.Errorf("entry.Data = %q, want %q", entry.Data, "data")
}
if _, ok2 := pc.GetStale(hashKey(key), key, true); !ok2 {
t.Error("fresh entry should be usable on timeout")
}
}
// TestFileCacheRefreshCachedAt 测试 RefreshCachedAt 方法。
func TestFileCacheRefreshCachedAt(t *testing.T) {
fc := NewFileCache(10, 1024, 1*time.Hour)
path := "/test/refresh.txt"
data := []byte("test data")
// 设置缓存
_ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain")
// 获取原始 CachedAt 时间
entry, ok := fc.Get(path)
if !ok {
t.Fatal("Expected to find cached entry")
}
originalCachedAt := entry.CachedAt
// 等待一小段时间
time.Sleep(10 * time.Millisecond)
// 刷新 CachedAt
fc.RefreshCachedAt(path)
// 再次获取,验证 CachedAt 已更新
entry, ok = fc.Get(path)
if !ok {
t.Fatal("Expected to find cached entry after refresh")
}
if !entry.CachedAt.After(originalCachedAt) {
t.Errorf("CachedAt not updated: %v <= %v", entry.CachedAt, originalCachedAt)
}
}
// TestFileCacheRefreshCachedAtNonExistent 测试刷新不存在的条目。
func TestFileCacheRefreshCachedAtNonExistent(t *testing.T) {
fc := NewFileCache(10, 1024, 1*time.Hour)
// 刷新不存在的条目应该静默忽略
fc.RefreshCachedAt("/nonexistent/path")
// 验证没有副作用
stats := fc.Stats()
if stats.Entries != 0 {
t.Errorf("Expected 0 entries, got %d", stats.Entries)
}
}
// TestProxyCacheDeleteByPatternWithMethod 测试按模式和方法的删除。
func TestProxyCacheDeleteByPatternWithMethod(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
// 添加多个缓存条目,带不同方法前缀
pc.Set(hashKey("GET:/api/users"), "GET:/api/users", []byte("users"), nil, 200, 10*time.Minute)
pc.Set(hashKey("POST:/api/users"), "POST:/api/users", []byte("create"), nil, 200, 10*time.Minute)
pc.Set(hashKey("GET:/api/posts"), "GET:/api/posts", []byte("posts"), nil, 200, 10*time.Minute)
pc.Set(hashKey("DELETE:/api/users/1"), "DELETE:/api/users/1", []byte("delete"), nil, 200, 10*time.Minute)
// 删除所有 GET:/api/users* 的条目(模式匹配 OrigKey包含方法前缀
deleted := pc.DeleteByPatternWithMethod("GET:/api/users*", "GET")
if deleted != 1 {
t.Errorf("Expected 1 deleted, got %d", deleted)
}
// 验证 GET:/api/users 被删除
if _, ok, _ := pc.Get(hashKey("GET:/api/users"), "GET:/api/users"); ok {
t.Error("GET:/api/users should be deleted")
}
// 验证 POST:/api/users 还在
if _, ok, _ := pc.Get(hashKey("POST:/api/users"), "POST:/api/users"); !ok {
t.Error("POST:/api/users should still exist")
}
// 验证 GET:/api/posts 还在
if _, ok, _ := pc.Get(hashKey("GET:/api/posts"), "GET:/api/posts"); !ok {
t.Error("GET:/api/posts should still exist")
}
}
// TestProxyCacheDeleteByPatternAllMethods 测试删除所有方法。
func TestProxyCacheDeleteByPatternAllMethods(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
// 添加多个缓存条目
pc.Set(hashKey("GET:/api/test"), "GET:/api/test", []byte("get"), nil, 200, 10*time.Minute)
pc.Set(hashKey("POST:/api/test"), "POST:/api/test", []byte("post"), nil, 200, 10*time.Minute)
pc.Set(hashKey("PUT:/api/test"), "PUT:/api/test", []byte("put"), nil, 200, 10*time.Minute)
// 删除所有 *:/api/test* 的条目(不限制方法,使用 * 通配符)
deleted := pc.DeleteByPatternWithMethod("*", "")
if deleted != 3 {
t.Errorf("Expected 3 deleted, got %d", deleted)
}
}
// TestProxyCacheDeleteByPatternNoMatch 测试无匹配删除。
func TestProxyCacheDeleteByPatternNoMatch(t *testing.T) {
pc := NewProxyCache(nil, false, 0, 0, 0)
pc.Set(hashKey("GET:/api/users"), "GET:/api/users", []byte("users"), nil, 200, 10*time.Minute)
// 删除不匹配的模式
deleted := pc.DeleteByPatternWithMethod("/other/*", "")
if deleted != 0 {
t.Errorf("Expected 0 deleted, got %d", deleted)
}
// 验证原条目还在
if _, ok, _ := pc.Get(hashKey("GET:/api/users"), "GET:/api/users"); !ok {
t.Error("Original entry should still exist")
}
}