diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 90aee86..e82eb03 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -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) diff --git a/internal/cache/file_cache.go b/internal/cache/file_cache.go index 564981d..b13b19e 100644 --- a/internal/cache/file_cache.go +++ b/internal/cache/file_cache.go @@ -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()