//! Authentication Flow Smoke Tests //! //! Tests to validate JWT authentication, session management, and authorization. use super::common::*; use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm}; use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, iss: String, aud: String, exp: u64, iat: u64, roles: Vec, } fn get_jwt_secret() -> String { std::env::var("JWT_SECRET") .unwrap_or_else(|_| "dev_secret_key_change_in_production".to_string()) } fn create_test_token() -> Result> { let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); let claims = Claims { sub: "test_user".to_string(), iss: "foxhunt-api-gateway".to_string(), aud: "foxhunt-services".to_string(), iat: now, exp: now + 3600, // Valid for 1 hour roles: vec!["trader".to_string()], }; let token = encode( &Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(get_jwt_secret().as_bytes()), )?; Ok(token) } #[tokio::test] async fn test_jwt_token_generation() { let result = with_timeout(async { let token = create_test_token() .map_err(|e| Box::new(e) as Box)?; assert!(!token.is_empty(), "JWT token should not be empty"); assert!(token.split('.').count() == 3, "JWT should have 3 parts"); println!("✅ JWT token generation successful"); println!(" Token length: {} characters", token.len()); Ok(()) }).await; assert!(result.is_ok(), "JWT token generation failed: {:?}", result.err()); } #[tokio::test] async fn test_jwt_token_validation() { let result = with_timeout(async { // Generate token let token = create_test_token() .map_err(|e| Box::new(e) as Box)?; // Validate token let validation = Validation::new(Algorithm::HS256); let token_data = decode::( &token, &DecodingKey::from_secret(get_jwt_secret().as_bytes()), &validation, ) .map_err(|e| Box::new(e) as Box)?; assert_eq!(token_data.claims.sub, "test_user"); assert_eq!(token_data.claims.iss, "foxhunt-api-gateway"); assert_eq!(token_data.claims.aud, "foxhunt-services"); assert!(token_data.claims.roles.contains(&"trader".to_string())); println!("✅ JWT token validation successful"); println!(" User: {}", token_data.claims.sub); println!(" Roles: {:?}", token_data.claims.roles); Ok(()) }).await; assert!(result.is_ok(), "JWT token validation failed: {:?}", result.err()); } #[tokio::test] async fn test_jwt_token_expiration() { let result = with_timeout(async { // Create expired token let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); let expired_claims = Claims { sub: "test_user".to_string(), iss: "foxhunt-api-gateway".to_string(), aud: "foxhunt-services".to_string(), iat: now - 7200, // Issued 2 hours ago exp: now - 3600, // Expired 1 hour ago roles: vec!["trader".to_string()], }; let expired_token = encode( &Header::new(Algorithm::HS256), &expired_claims, &EncodingKey::from_secret(get_jwt_secret().as_bytes()), )?; // Try to validate expired token let validation = Validation::new(Algorithm::HS256); let result = decode::( &expired_token, &DecodingKey::from_secret(get_jwt_secret().as_bytes()), &validation, ); assert!(result.is_err(), "Expired token should not validate"); println!("✅ JWT expiration validation successful"); Ok(()) }).await; assert!(result.is_ok(), "JWT expiration test failed: {:?}", result.err()); } #[tokio::test] async fn test_jwt_invalid_signature() { let result = with_timeout(async { // Create token with one secret let token = create_test_token() .map_err(|e| Box::new(e) as Box)?; // Try to validate with different secret let validation = Validation::new(Algorithm::HS256); let wrong_secret = "wrong_secret_key"; let result = decode::( &token, &DecodingKey::from_secret(wrong_secret.as_bytes()), &validation, ); assert!(result.is_err(), "Token with invalid signature should not validate"); println!("✅ JWT signature validation successful"); Ok(()) }).await; assert!(result.is_ok(), "JWT signature validation test failed: {:?}", result.err()); } #[tokio::test] async fn test_redis_session_storage() { let result = with_timeout(async { use redis::AsyncCommands; let client = redis::Client::open(redis_url().as_str()) .map_err(|e| Box::new(e) as Box)?; let mut con = client.get_multiplexed_async_connection() .await .map_err(|e| Box::new(e) as Box)?; // Simulate session storage let session_key = "smoke_test:session:test_user"; let session_data = serde_json::json!({ "user_id": "test_user", "roles": ["trader"], "created_at": chrono::Utc::now().to_rfc3339(), }); // Store session with TTL con.set_ex::<_, _, ()>(session_key, session_data.to_string(), 3600) .await .map_err(|e| Box::new(e) as Box)?; // Retrieve session let retrieved: String = con.get(session_key) .await .map_err(|e| Box::new(e) as Box)?; let retrieved_json: serde_json::Value = serde_json::from_str(&retrieved)?; assert_eq!(retrieved_json["user_id"], "test_user"); // Cleanup con.del::<_, ()>(session_key) .await .map_err(|e| Box::new(e) as Box)?; println!("✅ Redis session storage successful"); Ok(()) }).await; skip_if_unavailable!("Redis", { result }); } #[tokio::test] async fn test_jwt_revocation_check() { let result = with_timeout(async { use redis::AsyncCommands; let client = redis::Client::open(redis_url().as_str()) .map_err(|e| Box::new(e) as Box)?; let mut con = client.get_multiplexed_async_connection() .await .map_err(|e| Box::new(e) as Box)?; // Create token let token = create_test_token() .map_err(|e| Box::new(e) as Box)?; // Simulate token revocation let jti = "smoke_test_token_id"; let revocation_key = format!("revoked_tokens:{}", jti); // Add to revocation list con.set_ex::<_, _, ()>(&revocation_key, "revoked", 3600) .await .map_err(|e| Box::new(e) as Box)?; // Check if token is revoked let is_revoked: bool = con.exists(&revocation_key) .await .map_err(|e| Box::new(e) as Box)?; assert!(is_revoked, "Token should be marked as revoked"); // Cleanup con.del::<_, ()>(&revocation_key) .await .map_err(|e| Box::new(e) as Box)?; println!("✅ JWT revocation check successful"); Ok(()) }).await; skip_if_unavailable!("Redis", { result }); } #[tokio::test] async fn test_rate_limiting() { let result = with_timeout(async { use redis::AsyncCommands; let client = redis::Client::open(redis_url().as_str()) .map_err(|e| Box::new(e) as Box)?; let mut con = client.get_multiplexed_async_connection() .await .map_err(|e| Box::new(e) as Box)?; // Simulate rate limiting let rate_limit_key = "smoke_test:rate_limit:test_user"; let window_seconds = 60; let max_requests = 100; // Increment counter let count: i32 = con.incr(&rate_limit_key, 1) .await .map_err(|e| Box::new(e) as Box)?; if count == 1 { // Set expiration on first request con.expire::<_, ()>(&rate_limit_key, window_seconds) .await .map_err(|e| Box::new(e) as Box)?; } // Check if rate limit exceeded let is_limited = count > max_requests; assert!(!is_limited, "Rate limit should not be exceeded on first request"); // Get TTL let ttl: i32 = con.ttl(&rate_limit_key) .await .map_err(|e| Box::new(e) as Box)?; assert!(ttl > 0 && ttl <= window_seconds, "TTL should be set correctly"); // Cleanup con.del::<_, ()>(&rate_limit_key) .await .map_err(|e| Box::new(e) as Box)?; println!("✅ Rate limiting check successful"); println!(" Request count: {}/{}", count, max_requests); println!(" TTL: {} seconds", ttl); Ok(()) }).await; skip_if_unavailable!("Redis", { result }); } #[tokio::test] async fn test_authentication_flow_complete() { println!("🔐 Running complete authentication flow..."); let mut steps_passed = 0; let total_steps = 4; // Step 1: Generate token if create_test_token().is_ok() { println!(" ✓ JWT token generation"); steps_passed += 1; } else { println!(" ✗ JWT token generation failed"); } // Step 2: Validate token if let Ok(token) = create_test_token() { let validation = Validation::new(Algorithm::HS256); if decode::( &token, &DecodingKey::from_secret(get_jwt_secret().as_bytes()), &validation, ).is_ok() { println!(" ✓ JWT token validation"); steps_passed += 1; } else { println!(" ✗ JWT token validation failed"); } } // Step 3: Session storage if let Ok(client) = redis::Client::open(redis_url().as_str()) { if let Ok(mut con) = client.get_multiplexed_async_connection().await { use redis::AsyncCommands; let test_key = "smoke_test:auth_flow:session"; if con.set_ex::<_, _, ()>(test_key, "test", 60).await.is_ok() { println!(" ✓ Session storage"); steps_passed += 1; let _ = con.del::<_, ()>(test_key).await; } } } else { println!(" ⚠ Session storage (Redis unavailable)"); } // Step 4: Rate limiting if let Ok(client) = redis::Client::open(redis_url().as_str()) { if let Ok(mut con) = client.get_multiplexed_async_connection().await { use redis::AsyncCommands; let test_key = "smoke_test:auth_flow:rate_limit"; if con.incr::<_, _, i32>(test_key, 1).await.is_ok() { println!(" ✓ Rate limiting"); steps_passed += 1; let _ = con.del::<_, ()>(test_key).await; } } } else { println!(" ⚠ Rate limiting (Redis unavailable)"); } println!("\n📊 Authentication Flow Summary:"); println!(" Steps passed: {}/{}", steps_passed, total_steps); assert!(steps_passed >= 2, "Authentication flow requires at least JWT functionality"); println!("✅ Authentication flow check complete"); }