//! Common test utilities for API Gateway integration tests use anyhow::Result; use jsonwebtoken::{encode, EncodingKey, Header}; use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; pub use api_gateway::auth::{Jti, JwtClaims}; /// Test JWT configuration pub struct TestJwtConfig { pub secret: String, pub issuer: String, pub audience: String, } impl Default for TestJwtConfig { fn default() -> Self { Self { secret: "test-secret-must-be-at-least-64-characters-long-for-security-validation-ok-1234567890".to_string(), issuer: "foxhunt-api-gateway".to_string(), audience: "foxhunt-services".to_string(), } } } /// Generate a valid JWT token for testing pub fn generate_test_token( user_id: &str, roles: Vec, permissions: Vec, ttl_seconds: u64, ) -> Result<(String, String)> { let config = TestJwtConfig::default(); let jti = Jti::new(); let now = SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs(); let claims = JwtClaims { jti: jti.0.clone(), sub: user_id.to_string(), iat: now, exp: now + ttl_seconds, nbf: Some(now), // Not before: valid from now iss: config.issuer, aud: config.audience, roles, permissions, token_type: "access".to_string(), session_id: Some(Uuid::new_v4().to_string()), }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(config.secret.as_bytes()), )?; Ok((token, jti.0)) } /// Generate an expired JWT token for testing pub fn generate_expired_token(user_id: &str) -> Result { let config = TestJwtConfig::default(); let now = SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs(); let claims = JwtClaims { jti: Jti::new().0, sub: user_id.to_string(), iat: now - 7200, exp: now - 3600, // Expired 1 hour ago nbf: Some(now - 7200), // Not before: from 2 hours ago iss: config.issuer, aud: config.audience, roles: vec!["trader".to_string()], permissions: vec!["api.access".to_string()], token_type: "access".to_string(), session_id: Some(Uuid::new_v4().to_string()), }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(config.secret.as_bytes()), )?; Ok(token) } /// Generate a token with invalid signature pub fn generate_invalid_signature_token(user_id: &str) -> Result { let now = SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs(); let claims = JwtClaims { jti: Jti::new().0, sub: user_id.to_string(), iat: now, exp: now + 3600, nbf: Some(now), // Not before: valid from now iss: "foxhunt-api-gateway".to_string(), aud: "foxhunt-services".to_string(), roles: vec!["trader".to_string()], permissions: vec!["api.access".to_string()], token_type: "access".to_string(), session_id: Some(Uuid::new_v4().to_string()), }; // Use wrong secret to create invalid signature let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(b"wrong-secret-key-that-will-fail-validation-checks-completely"), )?; Ok(token) } /// Add timeout parameters to Redis URL for reliable test execution fn add_redis_timeouts(redis_url: &str) -> String { // Add connection and response timeouts to prevent indefinite hangs // connection_timeout=5 (5 seconds for connection establishment) // response_timeout=10 (10 seconds for Redis operations) if redis_url.contains('?') { format!("{}&connection_timeout=5&response_timeout=10", redis_url) } else { format!("{}?connection_timeout=5&response_timeout=10", redis_url) } } /// Wait for Redis to be ready pub async fn wait_for_redis(redis_url: &str, max_attempts: usize) -> Result<()> { let redis_url_with_timeout = add_redis_timeouts(redis_url); for attempt in 1..=max_attempts { match redis::Client::open(redis_url_with_timeout.as_str()) { Ok(client) => { match client.get_multiplexed_async_connection().await { Ok(mut conn) => { // Use redis::cmd to send PING command if let Ok(_) = redis::cmd("PING").query_async::(&mut conn).await { println!("✓ Redis ready after {} attempts", attempt); return Ok(()); } } Err(e) => { if attempt == max_attempts { return Err(anyhow::anyhow!("Redis not ready: {}", e)); } tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } } } Err(e) => { if attempt == max_attempts { return Err(anyhow::anyhow!("Failed to create Redis client: {}", e)); } tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } } } Err(anyhow::anyhow!("Redis not ready after {} attempts", max_attempts)) } /// Clean up Redis test data pub async fn cleanup_redis(redis_url: &str) -> Result<()> { let redis_url_with_timeout = add_redis_timeouts(redis_url); let client = redis::Client::open(redis_url_with_timeout.as_str())?; let mut conn = client.get_multiplexed_async_connection().await?; // Delete all keys matching test patterns let _: () = redis::cmd("FLUSHDB").query_async(&mut conn).await?; Ok(()) }