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
192 lines
5.1 KiB
Go
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)
|
|
}
|
|
}
|