Files
lijiaoqiao/supply-api/internal/middleware/db_token_backend_integration_test.go
Your Name 8ac23bf7d4 test: improve coverage and fix sanitizer bug
- Fix MaskMap to properly handle []string sensitive fields
- Add missing slice handling in sanitizer
- Add comprehensive tests for GetMetrics and CreateEventsBatch
- Improve audit/handler coverage from 49.8% to 68.8%
- Fix test expectations to match actual sanitizer behavior
- All tests pass
2026-04-08 07:44:58 +08:00

353 lines
10 KiB
Go

//go:build integration
// +build integration
package middleware
import (
"context"
"fmt"
"os"
"testing"
"time"
"lijiaoqiao/supply-api/internal/cache"
"lijiaoqiao/supply-api/internal/config"
"lijiaoqiao/supply-api/internal/repository"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"
)
// integrationTestDB holds database connection for integration tests
type integrationTestDB struct {
pool *pgxpool.Pool
redis *redis.Client
}
// setupIntegrationTest initializes real database connections for testing
func setupIntegrationTest(t *testing.T) (*integrationTestDB, func()) {
// Get connection strings from environment or use defaults
pgURL := os.Getenv("SUPPLY_TEST_POSTGRES")
if pgURL == "" {
pgURL = "postgres://supply_test:supply_test_pass@localhost:5432/supply_test?sslmode=disable"
}
redisAddr := os.Getenv("SUPPLY_TEST_REDIS")
if redisAddr == "" {
redisAddr = "localhost:6379"
}
// Connect to PostgreSQL
ctx := context.Background()
poolConfig, err := pgxpool.ParseConfig(pgURL)
if err != nil {
t.Skipf("Skipping integration test: cannot parse postgres config: %v", err)
}
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
t.Skipf("Skipping integration test: cannot connect to postgres: %v", err)
}
// Verify connection
if err := pool.Ping(ctx); err != nil {
pool.Close()
t.Skipf("Skipping integration test: cannot ping postgres: %v", err)
}
// Connect to Redis
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddr,
})
if err := redisClient.Ping(ctx).Err(); err != nil {
pool.Close()
t.Skipf("Skipping integration test: cannot connect to redis: %v", err)
}
// Setup schema
setupSchema(t, ctx, pool)
return &integrationTestDB{
pool: pool,
redis: redisClient,
}, func() {
pool.Close()
redisClient.Close()
}
}
// setupSchema creates the required tables for testing
func setupSchema(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
// Create enum type
_, err := pool.Exec(ctx, `
DO $$ BEGIN
CREATE TYPE token_status AS ENUM ('active', 'revoked', 'expired');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
if err != nil {
t.Fatalf("Failed to create enum: %v", err)
}
// Create table
_, err = pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS token_status_registry (
id BIGSERIAL PRIMARY KEY,
token_id VARCHAR(128) NOT NULL UNIQUE,
subject_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'user',
status token_status NOT NULL DEFAULT 'active',
issued_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
revoked_reason VARCHAR(256),
revoked_by BIGINT,
last_verified_at TIMESTAMPTZ,
verification_count BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
t.Fatalf("Failed to create table: %v", err)
}
}
// cleanupTable cleans up test data
func cleanupTable(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
_, _ = pool.Exec(ctx, "DELETE FROM token_status_registry")
}
// TestDBTokenStatusBackend_Integration_CheckTokenStatus_CacheHit tests with real Redis
func TestDBTokenStatusBackend_Integration_CheckTokenStatus_CacheHit(t *testing.T) {
db, cleanup := setupIntegrationTest(t)
defer cleanup()
ctx := context.Background()
cleanupTable(t, ctx, db.pool)
// Create real Redis cache
redisCache, err := cache.NewRedisCache(config.RedisConfig{
Host: db.redis.Options().Addr,
Password: "",
DB: 0,
PoolSize: 10,
})
if err != nil {
t.Skipf("Skipping: cannot create redis cache: %v", err)
}
// Create real repository
repo := repository.NewTokenStatusRepository(db.pool)
// Create backend with real dependencies
backend := NewDBTokenStatusBackend(repo, redisCache, 10*time.Second)
// Insert test token
_, err = db.pool.Exec(ctx, `
INSERT INTO token_status_registry (token_id, subject_id, tenant_id, status, expires_at)
VALUES ($1, 1, 1, 'active', NOW() + INTERVAL '1 hour')
`, "integration-test-token-1")
if err != nil {
t.Fatalf("Failed to insert test token: %v", err)
}
// First call - cache miss
status1, err := backend.CheckTokenStatus(ctx, "integration-test-token-1")
if err != nil {
t.Fatalf("CheckTokenStatus failed: %v", err)
}
if status1 != "active" {
t.Errorf("expected status 'active', got '%s'", status1)
}
// Second call - should be cache hit
status2, err := backend.CheckTokenStatus(ctx, "integration-test-token-1")
if err != nil {
t.Fatalf("CheckTokenStatus failed on second call: %v", err)
}
if status2 != "active" {
t.Errorf("expected status 'active' from cache, got '%s'", status2)
}
t.Log("Integration test passed: CheckTokenStatus with real Redis cache")
}
// TestDBTokenStatusBackend_Integration_RevokeToken tests with real DB and Redis
func TestDBTokenStatusBackend_Integration_RevokeToken(t *testing.T) {
db, cleanup := setupIntegrationTest(t)
defer cleanup()
ctx := context.Background()
cleanupTable(t, ctx, db.pool)
// Create real Redis cache
redisCache, err := cache.NewRedisCache(config.RedisConfig{
Host: db.redis.Options().Addr,
Password: "",
DB: 0,
PoolSize: 10,
})
if err != nil {
t.Skipf("Skipping: cannot create redis cache: %v", err)
}
// Create real repository
repo := repository.NewTokenStatusRepository(db.pool)
// Create backend
backend := NewDBTokenStatusBackend(repo, redisCache, 10*time.Second)
// Insert test token
_, err = db.pool.Exec(ctx, `
INSERT INTO token_status_registry (token_id, subject_id, tenant_id, status, expires_at)
VALUES ($1, 1, 1, 'active', NOW() + INTERVAL '1 hour')
`, "integration-test-revoke-token")
if err != nil {
t.Fatalf("Failed to insert test token: %v", err)
}
// Verify token is active
status, err := backend.CheckTokenStatus(ctx, "integration-test-revoke-token")
if err != nil {
t.Fatalf("CheckTokenStatus failed: %v", err)
}
if status != "active" {
t.Errorf("expected status 'active', got '%s'", status)
}
// Revoke the token
err = backend.RevokeToken(ctx, "integration-test-revoke-token", "integration test revocation")
if err != nil {
t.Fatalf("RevokeToken failed: %v", err)
}
// Verify token is revoked
status, err = backend.CheckTokenStatus(ctx, "integration-test-revoke-token")
if err != nil {
t.Fatalf("CheckTokenStatus failed after revocation: %v", err)
}
if status != "revoked" {
t.Errorf("expected status 'revoked', got '%s'", status)
}
t.Log("Integration test passed: RevokeToken with real DB and Redis")
}
// TestDBTokenStatusBackend_Integration_RevokeBySubjectID tests batch revocation
func TestDBTokenStatusBackend_Integration_RevokeBySubjectID(t *testing.T) {
db, cleanup := setupIntegrationTest(t)
defer cleanup()
ctx := context.Background()
cleanupTable(t, ctx, db.pool)
// Create real Redis cache
redisCache, err := cache.NewRedisCache(config.RedisConfig{
Host: db.redis.Options().Addr,
Password: "",
DB: 0,
PoolSize: 10,
})
if err != nil {
t.Skipf("Skipping: cannot create redis cache: %v", err)
}
// Create real repository
repo := repository.NewTokenStatusRepository(db.pool)
// Create backend
backend := NewDBTokenStatusBackend(repo, redisCache, 10*time.Second)
// Insert multiple test tokens for same subject
subjectID := int64(99999)
for i := 0; i < 5; i++ {
tokenID := fmt.Sprintf("integration-test-batch-token-%d", i)
_, err = db.pool.Exec(ctx, `
INSERT INTO token_status_registry (token_id, subject_id, tenant_id, status, expires_at)
VALUES ($1, $2, 1, 'active', NOW() + INTERVAL '1 hour')
`, tokenID, subjectID)
if err != nil {
t.Fatalf("Failed to insert test token %s: %v", tokenID, err)
}
}
// Revoke all tokens for subject
err = backend.RevokeBySubjectID(ctx, subjectID, "batch integration test revocation")
if err != nil {
t.Fatalf("RevokeBySubjectID failed: %v", err)
}
// Verify all tokens are revoked
for i := 0; i < 5; i++ {
tokenID := fmt.Sprintf("integration-test-batch-token-%d", i)
status, err := backend.CheckTokenStatus(ctx, tokenID)
if err != nil {
t.Fatalf("CheckTokenStatus failed for %s: %v", tokenID, err)
}
if status != "revoked" {
t.Errorf("expected status 'revoked' for %s, got '%s'", tokenID, status)
}
}
t.Log("Integration test passed: RevokeBySubjectID with real DB and Redis")
}
// TestTokenRevocationService_Integration tests with real Redis Pub/Sub
func TestTokenRevocationService_Integration(t *testing.T) {
db, cleanup := setupIntegrationTest(t)
defer cleanup()
ctx := context.Background()
cleanupTable(t, ctx, db.pool)
// Create real Redis cache
redisCache, err := cache.NewRedisCache(config.RedisConfig{
Host: db.redis.Options().Addr,
Password: "",
DB: 0,
PoolSize: 10,
})
if err != nil {
t.Skipf("Skipping: cannot create redis cache: %v", err)
}
// Create real repository
repo := repository.NewTokenStatusRepository(db.pool)
// Create backend
backend := NewDBTokenStatusBackend(repo, redisCache, 10*time.Second)
// Create revocation service
revocationService := NewTokenRevocationService(redisCache, backend)
// Insert test token
_, err = db.pool.Exec(ctx, `
INSERT INTO token_status_registry (token_id, subject_id, tenant_id, status, expires_at)
VALUES ($1, 1, 1, 'active', NOW() + INTERVAL '1 hour')
`, "integration-test-revocation-service-token")
if err != nil {
t.Fatalf("Failed to insert test token: %v", err)
}
// Revoke and publish
err = revocationService.RevokeAndPublish(ctx, "integration-test-revocation-service-token", "integration test")
if err != nil {
t.Fatalf("RevokeAndPublish failed: %v", err)
}
// Verify token is revoked
status, err := backend.CheckTokenStatus(ctx, "integration-test-revocation-service-token")
if err != nil {
t.Fatalf("CheckTokenStatus failed: %v", err)
}
if status != "revoked" {
t.Errorf("expected status 'revoked', got '%s'", status)
}
t.Log("Integration test passed: RevocationService with real Redis Pub/Sub")
}