//! Test utilities for TLI integration tests //! //! Provides JWT token generation and other test helpers. use anyhow::Result; use jsonwebtoken::{encode, EncodingKey, Header}; use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; /// JWT claims structure for testing #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestJwtClaims { /// JWT ID (unique identifier for revocation) pub jti: String, /// Subject (user ID) pub sub: String, /// Issued at timestamp pub iat: u64, /// Expiration timestamp pub exp: u64, /// Not before timestamp (optional) #[serde(skip_serializing_if = "Option::is_none")] pub nbf: Option, /// Issuer pub iss: String, /// Audience pub aud: String, /// User roles pub roles: Vec, /// Permissions pub permissions: Vec, /// Token type (access/refresh) pub token_type: String, /// Session ID (optional) #[serde(skip_serializing_if = "Option::is_none")] pub session_id: Option, } /// Test JWT configuration pub struct TestJwtConfig { pub secret: String, pub issuer: String, pub audience: String, } impl Default for TestJwtConfig { fn default() -> Self { Self { // Use same secret as API Gateway tests for compatibility secret: "test-secret-must-be-at-least-64-characters-long-for-security-validation-ok-1234567890".to_string(), issuer: "foxhunt-api".to_string(), audience: "foxhunt-services".to_string(), } } } /// Generate a valid JWT token for testing /// /// Returns (token, jti) for token tracking in tests. /// /// # Arguments /// * `user_id` - User identifier (e.g., "user123") /// * `roles` - User roles (e.g., vec!["trader".to_string()]) /// * `permissions` - User permissions (e.g., vec!["api.access".to_string()]) /// * `ttl_seconds` - Time to live in seconds (e.g., 3600 for 1 hour) /// /// # Example /// ```rust,ignore /// let (token, jti) = generate_test_jwt_token( /// "user123", /// vec!["trader".to_string()], /// vec!["api.access".to_string()], /// 3600, // 1 hour /// )?; /// ``` pub fn generate_test_jwt_token( user_id: &str, roles: Vec, permissions: Vec, 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)?.as_secs(); let claims = TestJwtClaims { jti: jti.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)) } /// Generate an expired JWT token for testing token expiration logic pub fn generate_expired_jwt_token(user_id: &str) -> Result { let config = TestJwtConfig::default(); let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); let claims = TestJwtClaims { jti: Uuid::new_v4().to_string(), sub: user_id.to_string(), iat: now - 7200, // Issued 2 hours ago 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 refresh token (similar to access token but with different type) pub fn generate_test_refresh_token(user_id: &str, 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)?.as_secs(); let claims = TestJwtClaims { jti: jti.clone(), sub: user_id.to_string(), iat: now, exp: now + ttl_seconds, nbf: Some(now), iss: config.issuer, aud: config.audience, roles: vec!["trader".to_string()], permissions: vec!["api.access".to_string()], 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()), )?; Ok((token, jti)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_generate_jwt_token_format() { let (token, jti) = generate_test_jwt_token( "test_user", vec!["trader".to_string()], vec!["api.access".to_string()], 3600, ) .unwrap(); // JWT should have 3 parts (header.payload.signature) let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3, "JWT should have 3 parts"); // JTI should be a valid UUID assert!(Uuid::parse_str(&jti).is_ok(), "JTI should be valid UUID"); } #[test] fn test_generate_expired_token() { let token = generate_expired_jwt_token("expired_user").unwrap(); // JWT should have 3 parts let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3, "JWT should have 3 parts"); } #[test] fn test_generate_refresh_token() { let (token, jti) = generate_test_refresh_token("refresh_user", 7200).unwrap(); // JWT should have 3 parts let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3, "JWT should have 3 parts"); // JTI should be a valid UUID assert!(Uuid::parse_str(&jti).is_ok(), "JTI should be valid UUID"); } }