refactor(cache): 提取 MatchRule 辅助函数并扩展测试
将 MatchRule 拆分为 matchPath/matchMethod/matchStatus 三个辅助函数, 提升代码可读性和可测试性。新增测试覆盖: - AcquireLockWithTimeout 锁超时机制 - RefreshTTL TTL 刷新功能 - SetValidationHeaders 验证头设置 - MatchRule 路径匹配变体(前缀/通配符/精确) - min_uses 计数阈值 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0a7f7170d5
commit
d933c1bd98
124
internal/cache/cache_test.go
vendored
124
internal/cache/cache_test.go
vendored
@ -88,6 +88,130 @@ func TestFileCacheLRUEviction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireLockWithTimeout(t *testing.T) {
|
||||
pc := NewProxyCache(nil, true, 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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
key := hashKey("minuses-test")
|
||||
origKey := "minuses-test"
|
||||
|
||||
// 首次设置
|
||||
pc.Set(key, origKey, []byte("data"), nil, 200, 10*time.Minute)
|
||||
|
||||
// 首次 Get,Uses = 1
|
||||
entry, _, _ := pc.Get(key, origKey)
|
||||
if entry.Uses.Load() != 1 {
|
||||
t.Errorf("Expected Uses=1 after first Get, got %d", entry.Uses.Load())
|
||||
}
|
||||
|
||||
// 第二次 Get,Uses = 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)
|
||||
|
||||
76
internal/cache/file_cache.go
vendored
76
internal/cache/file_cache.go
vendored
@ -493,44 +493,66 @@ func (c *ProxyCache) ReleaseLock(hashKey uint64, err error) {
|
||||
}
|
||||
|
||||
// MatchRule 检查请求是否匹配缓存规则。
|
||||
// 路径匹配规则:
|
||||
// - 以 "/" 结尾:前缀匹配(如 "/api/" 匹配 "/api/users")
|
||||
// - 包含 "*":通配符匹配(如 "/static/*" 匹配 "/static/css/style.css")
|
||||
// - 其他:精确匹配或带分隔符的前缀匹配(如 "/api" 匹配 "/api"、"/api/users"、"/api?query")
|
||||
func (c *ProxyCache) MatchRule(path, method string, status int) *ProxyCacheRule {
|
||||
for _, rule := range c.rules {
|
||||
// 检查路径匹配
|
||||
if rule.Path != "" {
|
||||
// 如果路径以 / 结尾,使用前缀匹配
|
||||
// 如果路径包含 *,使用通配符匹配
|
||||
// 否则使用前缀匹配(允许 /api 匹配 /api/users)
|
||||
if strings.HasSuffix(rule.Path, "/") {
|
||||
if !strings.HasPrefix(path, rule.Path) {
|
||||
continue
|
||||
}
|
||||
} else if strings.Contains(rule.Path, "*") {
|
||||
if !MatchPattern(rule.Path, path) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// 精确匹配或前缀匹配
|
||||
if path != rule.Path && !strings.HasPrefix(path, rule.Path+"/") && !strings.HasPrefix(path, rule.Path+"?") && len(path) <= len(rule.Path) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查方法
|
||||
if len(rule.Methods) > 0 && !slices.Contains(rule.Methods, method) {
|
||||
if !c.matchPath(rule.Path, path) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查状态码
|
||||
if len(rule.Statuses) > 0 && !slices.Contains(rule.Statuses, status) {
|
||||
if !c.matchMethod(rule.Methods, method) {
|
||||
continue
|
||||
}
|
||||
if !c.matchStatus(rule.Statuses, status) {
|
||||
continue
|
||||
}
|
||||
|
||||
return &rule
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchPath 检查路径是否匹配规则路径。
|
||||
func (c *ProxyCache) matchPath(rulePath, path string) bool {
|
||||
if rulePath == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(rulePath, "/"):
|
||||
// 前缀匹配:"/api/" 匹配 "/api/users"
|
||||
return strings.HasPrefix(path, rulePath)
|
||||
case strings.Contains(rulePath, "*"):
|
||||
// 通配符匹配:"/static/*" 匹配 "/static/css/style.css"
|
||||
return MatchPattern(rulePath, path)
|
||||
default:
|
||||
// 精确匹配或带分隔符的前缀匹配
|
||||
// "/api" 匹配 "/api"、"/api/users"、"/api?query"
|
||||
// 但不匹配 "/apiother"
|
||||
if path == rulePath {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(path, rulePath+"/") || strings.HasPrefix(path, rulePath+"?")
|
||||
}
|
||||
}
|
||||
|
||||
// matchMethod 检查方法是否在允许列表中。
|
||||
func (c *ProxyCache) matchMethod(methods []string, method string) bool {
|
||||
if len(methods) == 0 {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(methods, method)
|
||||
}
|
||||
|
||||
// matchStatus 检查状态码是否在允许列表中。
|
||||
func (c *ProxyCache) matchStatus(statuses []int, status int) bool {
|
||||
if len(statuses) == 0 {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(statuses, status)
|
||||
}
|
||||
|
||||
// Delete 删除缓存条目。
|
||||
func (c *ProxyCache) Delete(hashKey uint64) {
|
||||
c.mu.Lock()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user