Files
foxhunt/services/trading_service/tests/auth_helpers_tests.rs
jgrusewski d25c82f8f3 refactor: rename api_gateway → api across workspace, tests, and load crate
- Workspace Cargo.toml: remove web-gateway + api_gateway members, keep api
- trading_service: dep api-gateway → api, update test imports
- testing/api-gateway-load → testing/api-load (crate renamed)
- All test crates: get_api_gateway_addr → get_api_addr + variable renames

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:38:21 +01:00

350 lines
10 KiB
Rust

//! 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::<common::auth_helpers::TestJwtClaims>(
&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::<common::auth_helpers::TestJwtClaims>(
&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::<common::auth_helpers::TestJwtClaims>(
&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::<common::auth_helpers::TestJwtClaims>(
&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::<common::auth_helpers::TestJwtClaims>(
&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::<common::auth_helpers::TestJwtClaims>(
&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::<common::auth_helpers::TestJwtClaims>(
&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::<common::auth_helpers::TestJwtClaims>(
&token1,
&DecodingKey::from_secret(jwt_secret.as_ref()),
&validation,
)?;
let token2_data = decode::<common::auth_helpers::TestJwtClaims>(
&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(())
}