//! Test Utilities for Foxhunt HFT Trading System //! //! Provides reusable test helpers for E2E integration tests across all services. //! This module is only available when running tests. //! //! # Features //! //! - **JWT Token Generation**: Generate valid JWT tokens for authenticated gRPC requests //! - **Test User Credentials**: Create realistic test user profiles //! - **Token Metadata**: Track JTI for revocation tests //! //! # Usage //! //! ```rust,ignore //! use common::test_utils::{create_test_jwt_token, create_test_user_credentials}; //! //! #[tokio::test] //! async fn test_authenticated_request() { //! // Generate JWT token //! let (token, jti) = create_test_jwt_token().expect("Failed to create token"); //! //! // Add to gRPC metadata //! let mut request = tonic::Request::new(MyRequest { ... }); //! request.metadata_mut().insert( //! "authorization", //! format!("Bearer {}", token).parse().unwrap() //! ); //! //! // Make authenticated request //! let response = client.my_method(request).await?; //! } //! ``` use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(test)] use jsonwebtoken::{encode, EncodingKey, Header}; #[cfg(test)] use uuid::Uuid; /// JWT claims structure matching API Gateway expectations /// /// This structure mirrors the production JWT claims format to ensure /// test tokens are compatible with the authentication interceptor. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TestJwtClaims { /// JWT ID (unique identifier for revocation tracking) pub jti: String, /// Subject (user ID) pub sub: String, /// Issued at timestamp (Unix epoch seconds) pub iat: u64, /// Expiration timestamp (Unix epoch seconds) pub exp: u64, /// Not before timestamp (optional) #[serde(skip_serializing_if = "Option::is_none")] pub nbf: Option, /// Issuer (should be "foxhunt-api-gateway") pub iss: String, /// Audience (should be "foxhunt-services") pub aud: String, /// User roles (e.g., ["trader", "admin"]) pub roles: Vec, /// User permissions (e.g., ["api.access", "trade.execute"]) pub permissions: Vec, /// Token type: "access" or "refresh" pub token_type: String, /// Session ID for tracking related tokens (optional) #[serde(skip_serializing_if = "Option::is_none")] pub session_id: Option, } /// JWT configuration for test token generation /// /// Uses the same secret as API Gateway for compatibility. #[derive(Debug, Clone)] pub struct TestJwtConfig { /// JWT secret (must match API Gateway configuration) pub secret: String, /// JWT issuer (must match API Gateway configuration) pub issuer: String, /// JWT audience (must match API Gateway configuration) pub audience: String, } impl Default for TestJwtConfig { fn default() -> Self { Self { // Use same test secret as API Gateway (64+ chars for validation) secret: std::env::var("JWT_SECRET") .unwrap_or_else(|_| { "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(), } } } /// Test user credentials for authenticated requests /// /// Contains all necessary information for creating test users and tokens. #[derive(Debug, Clone)] pub struct TestUserCredentials { /// User identifier pub user_id: String, /// User roles pub roles: Vec, /// User permissions pub permissions: Vec, } impl Default for TestUserCredentials { /// Default test user with standard trader permissions fn default() -> Self { Self { user_id: "test_user_default".to_string(), roles: vec!["trader".to_string()], permissions: vec![ "api.access".to_string(), "trade.execute".to_string(), "trade.view".to_string(), ], } } } impl TestUserCredentials { /// Create a new test user with custom credentials pub fn new(user_id: impl Into, roles: Vec, permissions: Vec) -> Self { Self { user_id: user_id.into(), roles, permissions, } } /// Create an admin user with elevated permissions pub fn admin() -> Self { Self { user_id: "test_admin".to_string(), roles: vec!["admin".to_string(), "trader".to_string()], permissions: vec![ "api.access".to_string(), "admin.access".to_string(), "trade.execute".to_string(), "trade.view".to_string(), "trade.cancel".to_string(), "system.manage".to_string(), ], } } /// Create a read-only user pub fn read_only() -> Self { Self { user_id: "test_readonly".to_string(), roles: vec!["viewer".to_string()], permissions: vec!["api.access".to_string(), "trade.view".to_string()], } } /// Create a trader user (default permissions) pub fn trader() -> Self { Self::default() } } /// Generate a valid JWT access token for integration tests /// /// Returns `(token, jti)` where: /// - `token`: The JWT token string (use in Authorization header) /// - `jti`: The JWT ID for tracking/revocation /// /// # Default Configuration /// /// - **User ID**: "test_user_default" /// - **Roles**: ["trader"] /// - **Permissions**: ["api.access", "trade.execute", "trade.view"] /// - **TTL**: 3600 seconds (1 hour) /// /// # Example /// /// ```rust,ignore /// use common::test_utils::create_test_jwt_token; /// use tonic::metadata::MetadataValue; /// /// #[tokio::test] /// async fn test_authenticated_grpc_call() { /// let (token, _jti) = create_test_jwt_token() /// .expect("Failed to generate test token"); /// /// let mut request = tonic::Request::new(GetRegimeStateRequest { /// symbol: "ES.FUT".to_string(), /// }); /// /// // Add Authorization header /// request.metadata_mut().insert( /// "authorization", /// MetadataValue::from_str(&format!("Bearer {}", token)) /// .expect("Failed to create metadata value") /// ); /// /// let response = client.get_regime_state(request).await?; /// } /// ``` #[cfg(test)] pub fn create_test_jwt_token() -> Result<(String, String)> { let credentials = TestUserCredentials::default(); create_test_jwt_token_with_credentials(&credentials, 3600) } /// Generate a JWT token with custom user credentials /// /// Returns `(token, jti)` for token tracking. /// /// # Arguments /// /// - `credentials`: User credentials (user_id, roles, permissions) /// - `ttl_seconds`: Time-to-live in seconds (e.g., 3600 for 1 hour) /// /// # Example /// /// ```rust,ignore /// use common::test_utils::{create_test_jwt_token_with_credentials, TestUserCredentials}; /// /// #[tokio::test] /// async fn test_admin_endpoint() { /// let admin_creds = TestUserCredentials::admin(); /// let (token, _) = create_test_jwt_token_with_credentials(&admin_creds, 3600)?; /// /// // Use admin token for privileged operations /// // ... /// } /// ``` #[cfg(test)] pub fn create_test_jwt_token_with_credentials( credentials: &TestUserCredentials, ttl_seconds: u64, ) -> Result<(String, String)> { let config = TestJwtConfig::default(); let jti = Uuid::new_v4().to_string(); let now = SystemTime::now() .duration_since(UNIX_EPOCH) .context("Failed to get current timestamp")? .as_secs(); let claims = TestJwtClaims { jti: jti.clone(), sub: credentials.user_id.clone(), iat: now, exp: now + ttl_seconds, nbf: Some(now), iss: config.issuer, aud: config.audience, roles: credentials.roles.clone(), permissions: credentials.permissions.clone(), 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()), ) .context("Failed to encode JWT token")?; Ok((token, jti)) } /// Generate an expired JWT token for testing token expiration logic /// /// The returned token expired 1 hour ago and should be rejected by /// the authentication interceptor. /// /// # Example /// /// ```rust,ignore /// use common::test_utils::create_expired_jwt_token; /// /// #[tokio::test] /// async fn test_expired_token_rejection() { /// let expired_token = create_expired_jwt_token() /// .expect("Failed to generate expired token"); /// /// let mut request = tonic::Request::new(MyRequest { ... }); /// request.metadata_mut().insert( /// "authorization", /// format!("Bearer {}", expired_token).parse().unwrap() /// ); /// /// // Should return Unauthenticated error /// let result = client.my_method(request).await; /// assert!(result.is_err()); /// } /// ``` #[cfg(test)] pub fn create_expired_jwt_token() -> Result { let config = TestJwtConfig::default(); let credentials = TestUserCredentials::default(); let now = SystemTime::now() .duration_since(UNIX_EPOCH) .context("Failed to get current timestamp")? .as_secs(); let claims = TestJwtClaims { jti: Uuid::new_v4().to_string(), sub: credentials.user_id, iat: now - 7200, // Issued 2 hours ago exp: now - 3600, // Expired 1 hour ago nbf: Some(now - 7200), // Valid from 2 hours ago iss: config.issuer, aud: config.audience, roles: credentials.roles, permissions: credentials.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()), ) .context("Failed to encode expired JWT token")?; Ok(token) } /// Generate a JWT refresh token /// /// Refresh tokens have the same structure as access tokens but with /// `token_type: "refresh"` and typically longer TTL. /// /// # Example /// /// ```rust,ignore /// use common::test_utils::create_test_refresh_token; /// /// #[tokio::test] /// async fn test_token_refresh_flow() { /// let (refresh_token, _) = create_test_refresh_token() /// .expect("Failed to generate refresh token"); /// /// // Use refresh token to get new access token /// // ... /// } /// ``` #[cfg(test)] pub fn create_test_refresh_token() -> Result<(String, String)> { create_test_refresh_token_with_ttl(7200) // 2 hours default } /// Generate a JWT refresh token with custom TTL #[cfg(test)] pub fn create_test_refresh_token_with_ttl(ttl_seconds: u64) -> Result<(String, String)> { let config = TestJwtConfig::default(); let credentials = TestUserCredentials::default(); let jti = Uuid::new_v4().to_string(); let now = SystemTime::now() .duration_since(UNIX_EPOCH) .context("Failed to get current timestamp")? .as_secs(); let claims = TestJwtClaims { jti: jti.clone(), sub: credentials.user_id, iat: now, exp: now + ttl_seconds, nbf: Some(now), iss: config.issuer, aud: config.audience, roles: credentials.roles, permissions: credentials.permissions, token_type: "refresh".to_string(), session_id: Some(Uuid::new_v4().to_string()), }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(config.secret.as_bytes()), ) .context("Failed to encode refresh token")?; Ok((token, jti)) } /// Create test user credentials (convenience wrapper for TestUserCredentials::new) /// /// # Example /// /// ```rust,ignore /// use common::test_utils::create_test_user_credentials; /// /// let trader = create_test_user_credentials( /// "trader_001", /// vec!["trader".to_string()], /// vec!["api.access".to_string(), "trade.execute".to_string()], /// ); /// ``` pub fn create_test_user_credentials( user_id: impl Into, roles: Vec, permissions: Vec, ) -> TestUserCredentials { TestUserCredentials::new(user_id, roles, permissions) } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_credentials() { let creds = TestUserCredentials::default(); assert_eq!(creds.user_id, "test_user_default"); assert_eq!(creds.roles, vec!["trader"]); assert!(creds.permissions.contains(&"api.access".to_string())); assert!(creds.permissions.contains(&"trade.execute".to_string())); } #[test] fn test_admin_credentials() { let creds = TestUserCredentials::admin(); assert_eq!(creds.user_id, "test_admin"); assert!(creds.roles.contains(&"admin".to_string())); assert!(creds.permissions.contains(&"admin.access".to_string())); assert!(creds.permissions.contains(&"system.manage".to_string())); } #[test] fn test_readonly_credentials() { let creds = TestUserCredentials::read_only(); assert_eq!(creds.user_id, "test_readonly"); assert_eq!(creds.roles, vec!["viewer"]); assert!(creds.permissions.contains(&"api.access".to_string())); assert!(creds.permissions.contains(&"trade.view".to_string())); assert!(!creds.permissions.contains(&"trade.execute".to_string())); } #[test] fn test_create_jwt_token() { let result = create_test_jwt_token(); assert!(result.is_ok(), "Failed to create JWT token: {:?}", result); let (token, jti) = result.unwrap(); // JWT should have 3 parts (header.payload.signature) let parts: Vec<&str> = token.split('.').collect(); assert_eq!( parts.len(), 3, "JWT should have header.payload.signature format" ); // JTI should be a valid UUID assert!(Uuid::parse_str(&jti).is_ok(), "JTI should be a valid UUID"); // Token should not be empty assert!(!token.is_empty(), "Token should not be empty"); assert!(token.len() > 100, "Token should be reasonably long"); } #[test] fn test_create_jwt_token_with_custom_credentials() { let creds = TestUserCredentials::admin(); let result = create_test_jwt_token_with_credentials(&creds, 3600); assert!( result.is_ok(), "Failed to create JWT token with custom credentials" ); let (token, _jti) = result.unwrap(); let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3, "JWT should have 3 parts"); } #[test] fn test_create_expired_token() { let result = create_expired_jwt_token(); assert!(result.is_ok(), "Failed to create expired token"); let token = result.unwrap(); let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3, "JWT should have 3 parts"); } #[test] fn test_create_refresh_token() { let result = create_test_refresh_token(); assert!(result.is_ok(), "Failed to create refresh token"); let (token, jti) = result.unwrap(); let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3, "JWT should have 3 parts"); assert!(Uuid::parse_str(&jti).is_ok(), "JTI should be valid UUID"); } #[test] fn test_create_user_credentials() { let creds = create_test_user_credentials( "custom_user", vec!["custom_role".to_string()], vec!["custom.permission".to_string()], ); assert_eq!(creds.user_id, "custom_user"); assert_eq!(creds.roles, vec!["custom_role"]); assert_eq!(creds.permissions, vec!["custom.permission"]); } #[test] fn test_jwt_config_default() { let config = TestJwtConfig::default(); assert_eq!(config.issuer, "foxhunt-api-gateway"); assert_eq!(config.audience, "foxhunt-services"); assert!( config.secret.len() >= 64, "Secret should be at least 64 chars" ); } #[test] fn test_multiple_tokens_unique_jti() { let (_, jti1) = create_test_jwt_token().unwrap(); let (_, jti2) = create_test_jwt_token().unwrap(); assert_ne!(jti1, jti2, "Each token should have unique JTI"); } #[test] fn test_token_ttl_variations() { let short_ttl = create_test_jwt_token_with_credentials( &TestUserCredentials::default(), 300, // 5 minutes ); let long_ttl = create_test_jwt_token_with_credentials( &TestUserCredentials::default(), 86400, // 24 hours ); assert!(short_ttl.is_ok(), "Short TTL token should be created"); assert!(long_ttl.is_ok(), "Long TTL token should be created"); } }