- 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>
213 lines
6.1 KiB
Rust
213 lines
6.1 KiB
Rust
//! 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<u64>,
|
|
/// Issuer
|
|
pub iss: String,
|
|
/// Audience
|
|
pub aud: String,
|
|
/// User roles
|
|
pub roles: Vec<String>,
|
|
/// Permissions
|
|
pub permissions: Vec<String>,
|
|
/// Token type (access/refresh)
|
|
pub token_type: String,
|
|
/// Session ID (optional)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub session_id: Option<String>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
permissions: Vec<String>,
|
|
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<String> {
|
|
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");
|
|
}
|
|
}
|