326 lines
8.4 KiB
Go
326 lines
8.4 KiB
Go
|
|
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状态为active,TTL 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为active,TTL为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
|
|||
|
|
}
|