//! Tests for JWT Authentication Helper Module //! //! Validates that the auth_helpers module correctly generates JWT tokens //! and creates authenticated clients for E2E testing. mod common; use anyhow::Result; use common::auth_helpers::{ create_default_test_jwt, create_expired_test_jwt, create_invalid_issuer_jwt, create_test_jwt, get_api_addr, get_test_jwt_secret, get_test_user_id, TestAuthConfig, DEFAULT_API_GATEWAY_ADDR, DEFAULT_TEST_USER_ID, }; #[test] fn test_get_test_jwt_secret() { let secret = get_test_jwt_secret(); assert!(!secret.is_empty()); assert!( secret.len() >= 64, "JWT secret should be at least 64 characters" ); // Note: JWT_SECRET may be set in environment, so we don't assert exact value } #[test] fn test_get_test_user_id() { let user_id = get_test_user_id(); assert_eq!(user_id, DEFAULT_TEST_USER_ID); } #[test] fn test_get_api_addr() { let addr = get_api_addr(); assert!(!addr.is_empty()); assert!(addr.starts_with("http://")); assert_eq!(addr, DEFAULT_API_GATEWAY_ADDR); } #[test] fn test_create_default_test_jwt() -> Result<()> { let token = create_default_test_jwt()?; assert!(!token.is_empty()); assert_eq!( token.matches('.').count(), 2, "JWT should have 3 parts separated by 2 dots" ); // Verify token structure (header.payload.signature) let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3); assert!(!parts[0].is_empty(), "Header should not be empty"); assert!(!parts[1].is_empty(), "Payload should not be empty"); assert!(!parts[2].is_empty(), "Signature should not be empty"); Ok(()) } #[test] fn test_create_test_jwt_trader() -> Result<()> { let config = TestAuthConfig::trader(); let token = create_test_jwt(config)?; assert!(!token.is_empty()); assert_eq!(token.matches('.').count(), 2); // Decode and verify claims use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let token_data = decode::( &token, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; assert_eq!(token_data.claims.sub, DEFAULT_TEST_USER_ID); assert_eq!(token_data.claims.iss, "foxhunt-trading"); assert_eq!(token_data.claims.aud, "trading-api"); assert!(token_data.claims.roles.contains(&"trader".to_string())); assert!(!token_data.claims.jti.is_empty()); Ok(()) } #[test] fn test_create_test_jwt_admin() -> Result<()> { let config = TestAuthConfig::admin(); let token = create_test_jwt(config)?; assert!(!token.is_empty()); // Decode and verify admin claims use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let token_data = decode::( &token, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; assert_eq!(token_data.claims.sub, "test_admin_001"); assert!(token_data.claims.roles.contains(&"admin".to_string())); assert!(token_data .claims .permissions .iter() .any(|p| p.contains("admin"))); Ok(()) } #[test] fn test_create_test_jwt_viewer() -> Result<()> { let config = TestAuthConfig::viewer(); let token = create_test_jwt(config)?; assert!(!token.is_empty()); // Decode and verify viewer claims use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let token_data = decode::( &token, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; assert_eq!(token_data.claims.sub, "test_viewer_001"); assert!(token_data.claims.roles.contains(&"viewer".to_string())); assert!(token_data .claims .permissions .iter() .any(|p| p.contains("view"))); assert!(!token_data .claims .permissions .iter() .any(|p| p.contains("submit"))); Ok(()) } #[test] fn test_create_expired_test_jwt() -> Result<()> { let token = create_expired_test_jwt()?; assert!(!token.is_empty()); // Decode without expiry validation to check the token use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; // Disable expiry check validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let token_data = decode::( &token, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; // Verify the token is actually expired let now = chrono::Utc::now().timestamp() as usize; assert!( token_data.claims.exp < now, "Token should be expired (exp: {}, now: {})", token_data.claims.exp, now ); Ok(()) } #[test] fn test_create_invalid_issuer_jwt() -> Result<()> { let token = create_invalid_issuer_jwt()?; assert!(!token.is_empty()); // Try to decode with correct issuer validation - should fail use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let result = decode::( &token, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, ); assert!( result.is_err(), "Should fail validation due to wrong issuer" ); Ok(()) } #[test] fn test_auth_config_builder_pattern() { let config = TestAuthConfig::trader() .with_user_id("custom_user_123") .with_roles(vec!["custom_role".to_string()]) .with_permissions(vec!["custom.permission".to_string()]) .with_expiry(chrono::Duration::minutes(30)) .with_mfa_enabled(); assert_eq!(config.user_id, "custom_user_123"); assert_eq!(config.roles, vec!["custom_role"]); assert_eq!(config.permissions, vec!["custom.permission"]); assert_eq!(config.expiry_duration, chrono::Duration::minutes(30)); assert!(config.mfa_enabled); assert!(config.mfa_verified); } #[test] fn test_auth_config_mfa_unverified() { let config = TestAuthConfig::trader().with_mfa_unverified(); assert!(config.mfa_enabled); assert!(!config.mfa_verified); } #[test] fn test_default_auth_config() { let config = TestAuthConfig::default(); assert_eq!(config.user_id, DEFAULT_TEST_USER_ID); assert!(config.roles.contains(&"trader".to_string())); assert!(config.permissions.contains(&"api.access".to_string())); assert!(config.permissions.contains(&"trading.submit".to_string())); assert!(!config.mfa_enabled); } #[test] fn test_jwt_token_has_required_claims() -> Result<()> { let config = TestAuthConfig::trader(); let token = create_test_jwt(config)?; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let token_data = decode::( &token, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; // Verify all required claims are present assert!( !token_data.claims.jti.is_empty(), "jti (JWT ID) is required" ); assert!( !token_data.claims.sub.is_empty(), "sub (subject) is required" ); assert!(token_data.claims.exp > 0, "exp (expiry) is required"); assert!(token_data.claims.iat > 0, "iat (issued at) is required"); assert!( !token_data.claims.iss.is_empty(), "iss (issuer) is required" ); assert!( !token_data.claims.aud.is_empty(), "aud (audience) is required" ); assert!(!token_data.claims.roles.is_empty(), "roles are required"); assert_eq!(token_data.claims.token_type, "access"); assert!(token_data.claims.session_id.is_some()); Ok(()) } #[test] fn test_jwt_expiry_is_future() -> Result<()> { let token = create_default_test_jwt()?; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let token_data = decode::( &token, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; let now = chrono::Utc::now().timestamp() as usize; assert!(token_data.claims.exp > now, "Token should not be expired"); assert!( token_data.claims.iat <= now, "Token should be issued in the past or now" ); Ok(()) } #[test] fn test_multiple_tokens_have_unique_jti() -> Result<()> { let config = TestAuthConfig::trader(); let token1 = create_test_jwt(config.clone())?; let token2 = create_test_jwt(config)?; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; let mut validation = Validation::new(Algorithm::HS256); validation.set_issuer(&["foxhunt-trading"]); validation.set_audience(&["trading-api"]); let jwt_secret = get_test_jwt_secret(); let token1_data = decode::( &token1, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; let token2_data = decode::( &token2, &DecodingKey::from_secret(jwt_secret.as_ref()), &validation, )?; assert_ne!( token1_data.claims.jti, token2_data.claims.jti, "Each token should have a unique JTI" ); Ok(()) }