Files
foxhunt/testing/integration/smoke_tests/authentication_flow.rs
jgrusewski 08d070bb95 refactor: rename JWT issuer foxhunt-api-gateway → foxhunt-api, drop compat aliases
- JWT issuer now foxhunt-api across all 16 files (services, tests, config, docker-compose)
- Remove serde alias api_gateway_url from FxtConfig (no backwards compat)
- Remove api_gateway CLI alias from e2e orchestrator
- All services must deploy simultaneously for JWT validation to match

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

365 lines
12 KiB
Rust

//! 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<String>,
}
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<String, Box<dyn std::error::Error>> {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let claims = Claims {
sub: "test_user".to_string(),
iss: "foxhunt-api".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<dyn std::error::Error>)?;
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<dyn std::error::Error>)?;
// Validate token
let validation = Validation::new(Algorithm::HS256);
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_secret(get_jwt_secret().as_bytes()),
&validation,
)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
assert_eq!(token_data.claims.sub, "test_user");
assert_eq!(token_data.claims.iss, "foxhunt-api");
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".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::<Claims>(
&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<dyn std::error::Error>)?;
// Try to validate with different secret
let validation = Validation::new(Algorithm::HS256);
let wrong_secret = "wrong_secret_key";
let result = decode::<Claims>(
&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<dyn std::error::Error>)?;
let mut con = client.get_multiplexed_async_connection()
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
// 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<dyn std::error::Error>)?;
// Retrieve session
let retrieved: String = con.get(session_key)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
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<dyn std::error::Error>)?;
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<dyn std::error::Error>)?;
let mut con = client.get_multiplexed_async_connection()
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
// Create token
let token = create_test_token()
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
// 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<dyn std::error::Error>)?;
// Check if token is revoked
let is_revoked: bool = con.exists(&revocation_key)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
assert!(is_revoked, "Token should be marked as revoked");
// Cleanup
con.del::<_, ()>(&revocation_key)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
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<dyn std::error::Error>)?;
let mut con = client.get_multiplexed_async_connection()
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
// 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<dyn std::error::Error>)?;
if count == 1 {
// Set expiration on first request
con.expire::<_, ()>(&rate_limit_key, window_seconds)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
}
// 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<dyn std::error::Error>)?;
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<dyn std::error::Error>)?;
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::<Claims>(
&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");
}