Files
lijiaoqiao/gateway/internal/middleware/remote_runtime_cache_test.go
Your Name ae2b1bfe75 P3-A: Token Runtime 缓存层实现 - HTTPTimeout/LRU淘汰/命中率指标
Phase 3-A 完整实现,包含:

Gateway (lijiaoqiao/gateway):
- RemoteTokenRuntime 缓存实现: active=30s/expired=2m/revoked=10m TTL淘汰
- LRU 容量淘汰 (max_entries=10000,插入顺序淘汰)
- HTTPTimeoutConfig: 4个环境变量 (Dial/KeepAlive/Read/Write/MaxIdle)
- 缓存命中率指标: GetCacheHitRate() + 实例级别统计
- 上游延迟指标: RecordTokenRuntime() histogram
- buildTimeoutClient: 基于 HTTPTimeoutConfig 的 HTTP 客户端工厂
- 新增测试: 22个矩阵测试 (remote_runtime_matrix_test.go, config_test.go)

Platform Token Runtime (lijiaoqiao/platform-token-runtime):
- metrics/metrics.go: GetCacheHitRate() 方法
- inmemory_runtime.go: GetCacheHitRate() 实现

变更文件 (8 modified + 5 new):
- gateway/internal/middleware/remote_runtime.go    # 核心缓存实现
- gateway/internal/middleware/remote_runtime_test.go
- gateway/internal/middleware/remote_runtime_cache_test.go
- gateway/internal/middleware/remote_runtime_matrix_test.go
- gateway/internal/middleware/remote_runtime_metrics_test.go
- gateway/internal/metrics/metrics.go             # 新增
- gateway/internal/config/config.go                # HTTPTimeoutConfig
- gateway/internal/config/config_test.go
- gateway/internal/app/bootstrap.go                # 初始化顺序
- gateway/internal/router/router.go                # 指标注入
- platform-token-runtime/internal/metrics/metrics.go  # 新增
- platform-token-runtime/internal/app/bootstrap.go
- platform-token-runtime/internal/auth/service/inmemory_runtime.go
2026-04-21 17:27:51 +08:00

192 lines
5.1 KiB
Go

package middleware
import (
"context"
"io"
"net/http"
"strings"
"testing"
"time"
)
// P3-A-03/04: Test cache TTL eviction and max_entries eviction
func TestRemoteTokenRuntime_CacheTTL_Eviction(t *testing.T) {
// Fixed time for deterministic testing
fixedNow := time.Date(2026, 4, 21, 10, 0, 0, 0, time.UTC)
calls := 0
httpClient := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
calls++
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{
"data":{
"token_id":"tok-active",
"subject_id":"user-1",
"role":"admin",
"status":"active",
"scope":["read","write"],
"issued_at":"2026-04-21T09:00:00Z",
"expires_at":"2026-04-21T11:00:00Z"
}
}`)),
Header: make(http.Header),
}, nil
}),
}
// Create runtime with small cache TTL for testing: active=1s, expired=2s, revoked=3s
runtime := NewRemoteTokenRuntimeWithCacheConfig(
"http://tok.internal",
httpClient,
func() time.Time { return fixedNow },
CacheConfig{
ActiveTTL: 1 * time.Second,
ExpiredTTL: 2 * time.Second,
RevokedTTL: 3 * time.Second,
MaxEntries: 10000, // Large enough for this test
},
)
ctx := context.Background()
// First call - should hit upstream and cache
_, err := runtime.Verify(ctx, "raw-token-active")
if err != nil {
t.Fatalf("Verify failed: %v", err)
}
if calls != 1 {
t.Fatalf("expected 1 upstream call, got %d", calls)
}
// Second call within TTL - should hit cache
_, err = runtime.Verify(ctx, "raw-token-active")
if err != nil {
t.Fatalf("Verify failed: %v", err)
}
if calls != 1 {
t.Fatalf("expected 1 upstream call (cache hit), got %d", calls)
}
// Advance time beyond active TTL (1s)
runtime.AdvanceTime(2 * time.Second)
// Third call after TTL - should miss cache and call upstream
_, err = runtime.Verify(ctx, "raw-token-active")
if err != nil {
t.Fatalf("Verify failed after TTL expiry: %v", err)
}
if calls != 2 {
t.Fatalf("expected 2 upstream calls (TTL expired), got %d", calls)
}
}
func TestRemoteTokenRuntime_CacheMaxEntries_Eviction(t *testing.T) {
fixedNow := time.Date(2026, 4, 21, 10, 0, 0, 0, time.UTC)
callCount := 0
httpClient := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
callCount++
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{
"data":{
"token_id":"tok-evict",
"subject_id":"user-evict",
"role":"viewer",
"status":"active",
"scope":["read"],
"issued_at":"2026-04-21T09:00:00Z",
"expires_at":"2026-04-21T11:00:00Z"
}
}`)),
Header: make(http.Header),
}, nil
}),
}
// Create runtime with small MaxEntries to trigger eviction
runtime := NewRemoteTokenRuntimeWithCacheConfig(
"http://tok.internal",
httpClient,
func() time.Time { return fixedNow },
CacheConfig{
ActiveTTL: 10 * time.Second,
ExpiredTTL: 10 * time.Second,
RevokedTTL: 10 * time.Second,
MaxEntries: 3, // Small size to trigger eviction
},
)
ctx := context.Background()
// Fill cache beyond max entries
for i := 0; i < 5; i++ {
token := "raw-token-" + strings.TrimSpace(strings.Repeat("x", i))
_, err := runtime.Verify(ctx, token)
if err != nil {
t.Fatalf("Verify %d failed: %v", i, err)
}
}
// At this point, oldest entries should have been evicted
// A new call with a new token should trigger upstream call
_, err := runtime.Verify(ctx, "raw-token-new")
if err != nil {
t.Fatalf("Verify new token failed: %v", err)
}
// Should have called upstream because cache was full and oldest entry was evicted
if callCount != 6 {
t.Fatalf("expected 6 upstream calls (oldest entry evicted), got %d", callCount)
}
}
func TestRemoteTokenRuntime_CacheMetrics(t *testing.T) {
fixedNow := time.Date(2026, 4, 21, 10, 0, 0, 0, time.UTC)
httpClient := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{
"data":{
"token_id":"tok-metrics",
"subject_id":"user-metrics",
"role":"admin",
"status":"active",
"scope":["read","write"],
"issued_at":"2026-04-21T09:00:00Z",
"expires_at":"2026-04-21T11:00:00Z"
}
}`)),
Header: make(http.Header),
}, nil
}),
}
runtime := NewRemoteTokenRuntimeWithCacheConfig(
"http://tok.internal",
httpClient,
func() time.Time { return fixedNow },
DefaultCacheConfig(),
)
ctx := context.Background()
// First call - should miss cache and call upstream
runtime.Verify(ctx, "raw-token")
if h := runtime.GetCacheStats(); h.Cached != 0 || h.Upstream != 1 {
t.Fatalf("first call: expected cache miss (Cached=0, Upstream=1), got Cached=%d Upstream=%d", h.Cached, h.Upstream)
}
// Second call - should hit cache (same token, within TTL)
runtime.Verify(ctx, "raw-token")
if h := runtime.GetCacheStats(); h.Cached != 1 || h.Upstream != 1 {
t.Fatalf("second call: expected cache hit (Cached=1, Upstream=1), got Cached=%d Upstream=%d", h.Cached, h.Upstream)
}
}