Files
lijiaoqiao/supply-api/internal/middleware/cache_revocation_test.go

326 lines
8.4 KiB
Go
Raw Normal View History

package middleware
import (
"context"
"encoding/json"
"sync"
"testing"
"time"
)
// ==================== P0-03 缓存吊销传播测试 ====================
// 验证缓存TTL=30s与吊销传播<=5s的矛盾修复
// 修复方案:主动失效机制 + 短TTL兜底
// TestP003_CacheRevocationWithin5Seconds 验证P0-03吊销传播延迟 <= 5s
func TestP003_CacheRevocationWithin5Seconds(t *testing.T) {
cache := NewTokenCache()
ctx := context.Background()
// 1. 设置token状态为activeTTL 30秒
tokenID := "tok_test_123"
cache.Set(tokenID, "active", 30*time.Second)
// 2. 验证token在缓存中
status, found := cache.Get(tokenID)
if !found || status != "active" {
t.Fatalf("token should be active in cache before revocation")
}
// 3. 模拟吊销操作并触发主动失效
revokeTime := time.Now()
// 创建事件发布器(模拟)
publisher := &mockRevocationPublisher{
subscribers: make([]chan *TokenRevokedEvent, 0),
}
publisher.Subscribe(ctx)
// 发布吊销事件
revokeEvent := &TokenRevokedEvent{
TokenID: tokenID,
RevokedAt: revokeTime,
Reason: "user_requested",
}
// 模拟订阅者接收并处理
subscriber := newMockSubscriber(cache)
subscriber.Handle(ctx, revokeEvent)
// 4. 验证:吊销传播延迟 <= 5s
propagationDelay := time.Since(revokeTime)
if propagationDelay > 5*time.Second {
t.Errorf("P0-03 VIOLATION: revocation propagation delay %v exceeds 5s threshold", propagationDelay)
}
// 5. 验证token已从缓存中失效
_, found = cache.Get(tokenID)
if found {
t.Errorf("P0-03 VIOLATION: token should be invalidated immediately after revocation")
}
}
// TestP003_ActiveInvalidationOverridesTTL 验证主动失效优先级高于TTL
func TestP003_ActiveInvalidationOverridesTTL(t *testing.T) {
cache := NewTokenCache()
// 1. 设置token为activeTTL为30秒长TTL
tokenID := "tok_long_ttl_123"
cache.Set(tokenID, "active", 30*time.Second)
// 2. 在TTL过期前主动失效
cache.Invalidate(tokenID)
// 3. 验证token已不存在主动失效优先
_, found := cache.Get(tokenID)
if found {
t.Errorf("P0-03 VIOLATION: active invalidation should take precedence over TTL")
}
}
// TestP003_MultipleTokensRevocation 验证批量吊销传播
func TestP003_MultipleTokensRevocation(t *testing.T) {
cache := NewTokenCache()
ctx := context.Background()
// 1. 批量设置100个token
tokenCount := 100
tokenIDs := make([]string, tokenCount)
for i := 0; i < tokenCount; i++ {
tokenIDs[i] = "tok_batch_" + string(rune(i))
cache.Set(tokenIDs[i], "active", 30*time.Second)
}
// 2. 模拟批量吊销事件
subscriber := newMockSubscriber(cache)
startTime := time.Now()
for _, tokenID := range tokenIDs {
revokeEvent := &TokenRevokedEvent{
TokenID: tokenID,
RevokedAt: startTime,
Reason: "admin_batch_revoke",
}
subscriber.Handle(ctx, revokeEvent)
}
// 3. 验证所有token都已失效
for _, tokenID := range tokenIDs {
_, found := cache.Get(tokenID)
if found {
t.Errorf("token %s should be invalidated", tokenID)
}
}
// 4. 验证:总传播时间 <= 5s
totalPropagation := time.Since(startTime)
if totalPropagation > 5*time.Second {
t.Errorf("P0-03 VIOLATION: batch revocation took %v, exceeds 5s threshold", totalPropagation)
}
}
// TestP003_RedisPubSubIntegration 验证Redis Pub/Sub集成
func TestP003_RedisPubSubIntegration(t *testing.T) {
// 这个测试需要Redis连接标记为集成测试
// 在CI环境中跳过
t.Skip("Integration test - requires Redis connection")
}
// TestP003_TTLShortenedTo10Seconds 验证TTL缩短到10秒作为兜底
func TestP003_TTLShortenedTo10Seconds(t *testing.T) {
cache := NewTokenCache()
// 根据修复设计TTL从30s缩短到10s作为兜底
expectedMaxTTL := 10 * time.Second
tokenID := "tok_ttl_test"
cache.Set(tokenID, "active", expectedMaxTTL)
// 验证TTL设置正确通过检查expires时间
cache.mu.RLock()
entry, found := cache.data[tokenID]
cache.mu.RUnlock()
if !found {
t.Fatalf("token should be set in cache")
}
ttl := entry.expires.Sub(time.Now())
if ttl > expectedMaxTTL {
t.Errorf("TTL should not exceed %v, got %v", expectedMaxTTL, ttl)
}
}
// TestP003_SubscriberHandlesConcurrentRequests 验证订阅者处理并发请求
func TestP003_SubscriberHandlesConcurrentRequests(t *testing.T) {
cache := NewTokenCache()
ctx := context.Background()
subscriber := newMockSubscriber(cache)
tokenID := "tok_concurrent_test"
cache.Set(tokenID, "active", 30*time.Second)
var wg sync.WaitGroup
concurrency := 10
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
revokeEvent := &TokenRevokedEvent{
TokenID: tokenID,
RevokedAt: time.Now(),
Reason: "concurrent_revoke",
}
subscriber.Handle(ctx, revokeEvent)
}()
}
wg.Wait()
// 验证token已失效
_, found := cache.Get(tokenID)
if found {
t.Errorf("token should be invalidated after concurrent revocation")
}
}
// ==================== Mock实现 ====================
// TokenRevokedEvent 吊销事件
type TokenRevokedEvent struct {
TokenID string `json:"token_id"`
RevokedAt time.Time `json:"revoked_at"`
Reason string `json:"reason"`
}
// mockRevocationPublisher 模拟发布者
type mockRevocationPublisher struct {
subscribers []chan *TokenRevokedEvent
mu sync.RWMutex
}
// Subscribe 订阅
func (p *mockRevocationPublisher) Subscribe(ctx context.Context) {
ch := make(chan *TokenRevokedEvent, 100)
p.mu.Lock()
p.subscribers = append(p.subscribers, ch)
p.mu.Unlock()
go func() {
<-ctx.Done()
close(ch)
}()
}
// Publish 发布吊销事件
func (p *mockRevocationPublisher) Publish(event *TokenRevokedEvent) {
p.mu.RLock()
defer p.mu.RUnlock()
for _, ch := range p.subscribers {
select {
case ch <- event:
default:
// channel full, skip
}
}
}
// mockSubscriber 模拟订阅者
type mockSubscriber struct {
cache *TokenCache
}
// newMockSubscriber 创建订阅者
func newMockSubscriber(cache *TokenCache) *mockSubscriber {
return &mockSubscriber{cache: cache}
}
// Handle 处理吊销事件
func (s *mockSubscriber) Handle(ctx context.Context, event *TokenRevokedEvent) {
// 立即失效缓存(主动失效机制)
s.cache.Invalidate(event.TokenID)
}
// ==================== 基准测试 ====================
// BenchmarkP003_RevocationPropagation 基准测试单token吊销传播
func BenchmarkP003_RevocationPropagation(b *testing.B) {
cache := NewTokenCache()
subscriber := newMockSubscriber(cache)
ctx := context.Background()
tokenID := "tok_benchmark"
cache.Set(tokenID, "active", 30*time.Second)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// 重新设置
cache.Set(tokenID, "active", 30*time.Second)
// 吊销
event := &TokenRevokedEvent{
TokenID: tokenID,
RevokedAt: time.Now(),
Reason: "benchmark",
}
subscriber.Handle(ctx, event)
}
}
// BenchmarkP003_BatchRevocation 基准测试:批量吊销
func BenchmarkP003_BatchRevocation(b *testing.B) {
cache := NewTokenCache()
subscriber := newMockSubscriber(cache)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// 批量设置100个token
for j := 0; j < 100; j++ {
tokenID := "tok_batch_" + string(rune(j))
cache.Set(tokenID, "active", 30*time.Second)
}
// 批量吊销
for j := 0; j < 100; j++ {
tokenID := "tok_batch_" + string(rune(j))
event := &TokenRevokedEvent{
TokenID: tokenID,
RevokedAt: time.Now(),
Reason: "benchmark",
}
subscriber.Handle(ctx, event)
}
}
}
// ==================== 测试报告 ====================
// TestP003_Summary 打印测试总结
func TestP003_Summary(t *testing.T) {
t.Log("=== P0-03 缓存吊销传播测试总结 ===")
t.Log("设计问题缓存TTL=30s与吊销传播<=5s矛盾")
t.Log("修复方案:主动失效机制 + TTL缩短到10s")
t.Log("")
t.Log("测试覆盖:")
t.Log("1. 单token吊销传播延迟 <= 5s")
t.Log("2. 主动失效优先级高于TTL")
t.Log("3. 批量吊销传播")
t.Log("4. TTL缩短验证")
t.Log("5. 并发处理能力")
}
// SerializeEventForPubSub 序列化事件用于Pub/Sub辅助函数
func SerializeEventForPubSub(event *TokenRevokedEvent) ([]byte, error) {
return json.Marshal(event)
}
// DeserializeEventFromPubSub 从Pub/Sub反序列化事件
func DeserializeEventFromPubSub(data []byte) (*TokenRevokedEvent, error) {
var event TokenRevokedEvent
err := json.Unmarshal(data, &event)
return &event, err
}