diff --git a/Cargo.lock b/Cargo.lock index cc3b98df1..7f21cbcff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3525,6 +3525,7 @@ dependencies = [ "config", "criterion", "data", + "dotenvy", "futures", "hdrhistogram", "jsonwebtoken", @@ -9766,6 +9767,7 @@ dependencies = [ "config", "criterion", "data", + "database", "fastrand", "futures", "futures-util", diff --git a/docker-compose.yml b/docker-compose.yml index 2d0630fd0..ffa885087 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -167,6 +167,8 @@ services: context: . dockerfile: services/trading_service/Dockerfile container_name: foxhunt-trading-service + env_file: + - .env # Load JWT_SECRET and other config from .env (Wave 147) ports: - "50052:50051" # Map external 50052 to internal 50051 - "9092:9092" # Metrics @@ -203,6 +205,8 @@ services: context: . dockerfile: services/backtesting_service/Dockerfile container_name: foxhunt-backtesting-service + env_file: + - .env # Load JWT_SECRET and other config from .env (Wave 147) ports: - "50053:50053" # Map external 50053 to internal 50053 - "9093:9093" # Metrics @@ -248,6 +252,8 @@ services: dockerfile: services/ml_training_service/Dockerfile container_name: foxhunt-ml-training-service runtime: nvidia + env_file: + - .env # Load JWT_SECRET and other config from .env (Wave 147) ports: - "50054:50053" # Map external 50054 to internal 50053 - "9094:9094" # Metrics @@ -292,6 +298,8 @@ services: context: . dockerfile: services/api_gateway/Dockerfile container_name: foxhunt-api-gateway + env_file: + - .env # Load JWT_SECRET and other config from .env (Wave 147) ports: - "50051:50050" # Map external 50051 to internal 50050 - "9091:9091" # Metrics diff --git a/services/api_gateway/src/auth/jwt/service.rs b/services/api_gateway/src/auth/jwt/service.rs index d6d7c06d7..75f5b7108 100644 --- a/services/api_gateway/src/auth/jwt/service.rs +++ b/services/api_gateway/src/auth/jwt/service.rs @@ -85,8 +85,8 @@ impl JwtConfig { Ok(Self { jwt_secret, - jwt_issuer: "foxhunt-trading".to_string(), - jwt_audience: "trading-api".to_string(), + jwt_issuer: "foxhunt-api-gateway".to_string(), + jwt_audience: "foxhunt-services".to_string(), }) } @@ -410,8 +410,8 @@ mod tests { let config = JwtConfig::new().expect("Should create config with valid JWT_SECRET"); - assert_eq!(config.jwt_issuer, "foxhunt-trading"); - assert_eq!(config.jwt_audience, "trading-api"); + assert_eq!(config.jwt_issuer, "foxhunt-api-gateway"); + assert_eq!(config.jwt_audience, "foxhunt-services"); assert!(config.jwt_secret.len() >= 64); std::env::remove_var("JWT_SECRET"); diff --git a/services/trading_service/Cargo.toml b/services/trading_service/Cargo.toml index d70bf4878..c683571d4 100644 --- a/services/trading_service/Cargo.toml +++ b/services/trading_service/Cargo.toml @@ -92,6 +92,7 @@ data.workspace = true common = { workspace = true, features = ["database"] } storage = { workspace = true } config = { workspace = true, features = ["postgres"] } +database = { path = "../../database" } # Model functionality from storage and ml-data crates ml-data = { path = "../../ml-data" } semver.workspace = true diff --git a/services/trading_service/src/event_persistence.rs b/services/trading_service/src/event_persistence.rs index 273cfd541..cc278a2af 100644 --- a/services/trading_service/src/event_persistence.rs +++ b/services/trading_service/src/event_persistence.rs @@ -41,6 +41,24 @@ impl EventPersistence { } } + /// Create a test instance with mock database connection + /// This is used only in tests where actual database operations are not required + pub async fn new_for_testing() -> Result { + // Create a test database URL - uses SQLite for testing + let db_url = std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string()); + + let db_pool = sqlx::PgPool::connect(&db_url) + .await + .map_err(|e| anyhow::anyhow!("Failed to create test pool: {}", e))?; + + Ok(Self { + db_pool, + node_id: "test-node".to_string(), + process_id: 1, + }) + } + /// Persist a trading event to the database pub async fn write_event(&self, event: TradingEventData) -> Result { let now_ns = std::time::SystemTime::now() diff --git a/services/trading_service/src/repository_impls.rs b/services/trading_service/src/repository_impls.rs index f5495fad3..498887887 100644 --- a/services/trading_service/src/repository_impls.rs +++ b/services/trading_service/src/repository_impls.rs @@ -1218,15 +1218,226 @@ impl ConfigRepository for PostgresConfigRepository { Ok(()) } - async fn subscribe_to_changes(&self) -> TradingServiceResult { - let (_tx, rx) = tokio::sync::broadcast::channel(1000); - - // In production, this would use PostgreSQL LISTEN/NOTIFY - // For now, return a channel that can be used for config change notifications - tokio::spawn(async move { - // Placeholder - would implement PostgreSQL LISTEN here - }); - - Ok(rx) + async fn subscribe_to_changes(&self) -> TradingServiceResult { + let (_tx, rx) = tokio::sync::broadcast::channel(1000); + + // In production, this would use PostgreSQL LISTEN/NOTIFY + // For now, return a channel that can be used for config change notifications + tokio::spawn(async move { + // Placeholder - would implement PostgreSQL LISTEN here + }); + + Ok(rx) + } + } + + // ============================================================================= + // Mock Implementations for Testing + // ============================================================================= + + /// Mock implementation of TradingRepository for testing + #[derive(Debug, Clone, Default)] + pub struct MockTradingRepository; + + impl MockTradingRepository { + pub fn new() -> Self { + Self + } + } + + #[async_trait] + impl TradingRepository for MockTradingRepository { + async fn store_order(&self, _order: &TradingOrder) -> TradingServiceResult { + Ok(uuid::Uuid::new_v4().to_string()) + } + + async fn update_order_status( + &self, + _order_id: &str, + _status: common::types::OrderStatus, + ) -> TradingServiceResult<()> { + Ok(()) + } + + async fn get_order(&self, _order_id: &str) -> TradingServiceResult> { + Ok(None) + } + + async fn get_orders_for_account( + &self, + _account_id: &str, + ) -> TradingServiceResult> { + Ok(Vec::new()) + } + + async fn store_execution(&self, _execution: &crate::repositories::ExecutionEvent) -> TradingServiceResult<()> { + Ok(()) + } + + async fn get_execution_history( + &self, + _request: &GetExecutionHistoryRequest, + ) -> TradingServiceResult> { + Ok(Vec::new()) + } + async fn store_position(&self, _position: &TradingPosition) -> TradingServiceResult<()> { + Ok(()) + } + + async fn get_positions( + &self, + _account_id: Option<&str>, + _symbol: Option<&str>, + ) -> TradingServiceResult> { + Ok(Vec::new()) + } + + async fn get_portfolio_summary( + &self, + account_id: &str, + ) -> TradingServiceResult { + Ok(PortfolioSummary { + account_id: account_id.to_string(), + total_value: 0.0, + cash_balance: 0.0, + positions_value: 0.0, + unrealized_pnl: 0.0, + realized_pnl: 0.0, + }) + } + + async fn get_realized_pnl(&self, _account_id: &str, _symbol: Option<&str>) -> TradingServiceResult { + Ok(0.0) + } + + async fn get_day_pnl(&self, _account_id: &str) -> TradingServiceResult { + Ok(0.0) + } + } + + /// Mock implementation of MarketDataRepository for testing + #[derive(Debug, Clone, Default)] + pub struct MockMarketDataRepository; + + impl MockMarketDataRepository { + pub fn new() -> Self { + Self + } + } + + #[async_trait] + impl MarketDataRepository for MockMarketDataRepository { + async fn store_market_tick(&self, _tick: &MarketTick) -> TradingServiceResult<()> { + Ok(()) + } + + async fn get_order_book(&self, symbol: &str, _depth: i32) -> TradingServiceResult { + Ok(crate::repositories::OrderBook { + symbol: symbol.to_string(), + bids: Vec::new(), + asks: Vec::new(), + timestamp: chrono::Utc::now().timestamp(), + }) + } + + async fn store_order_book( + &self, + _symbol: &str, + _order_book: &crate::repositories::OrderBook, + ) -> TradingServiceResult<()> { + Ok(()) + } + async fn get_latest_prices(&self, _symbols: &[String]) -> TradingServiceResult> { + Ok(Vec::new()) + } + + async fn store_market_event(&self, _event: &common::MarketDataEvent) -> TradingServiceResult<()> { + Ok(()) + } + + async fn get_historical_data( + &self, + _symbol: &str, + _from: i64, + _to: i64, + ) -> TradingServiceResult> { + Ok(Vec::new()) + } + + async fn get_order_book_level_count(&self, _symbol: &str, _price: f64, _side: common::OrderSide) -> TradingServiceResult { + Ok(1) + } + } + + /// Mock implementation of RiskRepository for testing + #[derive(Debug, Clone, Default)] + pub struct MockRiskRepository; + + impl MockRiskRepository { + pub fn new() -> Self { + Self + } + } + + #[async_trait] + impl RiskRepository for MockRiskRepository { + async fn store_var_calculation( + &self, + _calculation: &VarCalculation, + ) -> TradingServiceResult<()> { + Ok(()) + } + + async fn get_risk_limits(&self, account_id: &str) -> TradingServiceResult { + Ok(RiskLimits { + account_id: account_id.to_string(), + max_order_size: 1000000.0, + max_position_limit: 10000000.0, + max_drawdown_limit: 0.10, + daily_loss_limit: Some(50000.0), + }) + } + + async fn update_risk_limits( + &self, + _account_id: &str, + _limits: &RiskLimits, + ) -> TradingServiceResult<()> { + Ok(()) + } + + async fn store_risk_alert(&self, _alert: &RiskAlert) -> TradingServiceResult<()> { + Ok(()) + } + + async fn get_risk_metrics(&self, account_id: &str) -> TradingServiceResult { + Ok(RiskMetrics { + account_id: account_id.to_string(), + current_var: 0.0, + current_drawdown: 0.0, + position_concentration: 0.0, + leverage_ratio: 1.0, + }) + } + + async fn store_position_risk( + &self, + _account_id: &str, + _symbol: &str, + _risk: &PositionRisk, + ) -> TradingServiceResult<()> { + Ok(()) + } + + async fn validate_order_risk( + &self, + _account_id: &str, + _order: &OrderRequest, + ) -> TradingServiceResult { + Ok(true) + } + + async fn calculate_margin_used(&self, _account_id: &str) -> TradingServiceResult { + Ok(0.0) + } } -} diff --git a/services/trading_service/src/state.rs b/services/trading_service/src/state.rs index 9ba535a5c..a4bf7e934 100644 --- a/services/trading_service/src/state.rs +++ b/services/trading_service/src/state.rs @@ -175,37 +175,38 @@ impl TradingServiceState { /// /// Note: This function is only available when building tests. pub async fn new_for_testing() -> TradingServiceResult { - // For testing, create a minimal repository setup + // For testing, create a minimal repository setup using PostgreSQL implementations use crate::repository_impls::{ - MockTradingRepository, MockMarketDataRepository, MockRiskRepository, + PostgresTradingRepository, PostgresMarketDataRepository, PostgresRiskRepository, PostgresConfigRepository, }; - use database::PostgresConfigRepository; use crate::event_persistence::EventPersistence; - - // Create mock repositories - let trading_repository = Arc::new(MockTradingRepository::new()) as Arc; - let market_data_repository = Arc::new(MockMarketDataRepository::new()) as Arc; - let risk_repository = Arc::new(MockRiskRepository::new()) as Arc; - - // Create minimal config repository (in-memory for testing) - // Note: PostgresConfigRepository needs a pool, we'll use a test database URL + use sqlx::PgPool; + + // Get database URL for testing let db_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string()); - - let pool = database::create_pool(&db_url) + + let pool = PgPool::connect(&db_url) .await .map_err(|e| crate::error::TradingServiceError::Internal { message: format!("Failed to create test database pool: {}", e), })?; - - let config_repository = Arc::new(PostgresConfigRepository::new(pool)); - + + // Create PostgreSQL repositories for testing + let trading_repository = Arc::new(PostgresTradingRepository::new(pool.clone())) as Arc; + let market_data_repository = Arc::new(PostgresMarketDataRepository::new(pool.clone())) as Arc; + let risk_repository = Arc::new(PostgresRiskRepository::new(pool.clone())) as Arc; + + // Create config repository using the same pool + let config_repository = Arc::new(PostgresConfigRepository::new(pool.clone())); + // Create event persistence with a test directory let event_persistence = Arc::new(EventPersistence::new_for_testing() + .await .map_err(|e| crate::error::TradingServiceError::Internal { message: format!("Failed to create event persistence: {}", e), })?); - + // Create state without kill switch or model cache for simplicity Self::new_with_repositories( trading_repository, diff --git a/tests/e2e/Cargo.toml b/tests/e2e/Cargo.toml index d37af9a8a..b0efe0040 100644 --- a/tests/e2e/Cargo.toml +++ b/tests/e2e/Cargo.toml @@ -37,6 +37,9 @@ uuid = { version = "1.0", features = ["v4", "serde"] } # JWT authentication jsonwebtoken = "9.3" +# Environment variables +dotenvy = "0.15" + # Numerical rust_decimal = { version = "1.32", features = ["serde-float"] } bigdecimal = "0.4" diff --git a/tests/e2e/src/framework.rs b/tests/e2e/src/framework.rs index a02423ecf..ad4134a47 100644 --- a/tests/e2e/src/framework.rs +++ b/tests/e2e/src/framework.rs @@ -82,6 +82,9 @@ impl E2ETestFramework { /// /// This token is accepted by API Gateway for test scenarios. fn generate_test_jwt_token() -> Result { + // Load .env file if present (development mode) + // Silent failure allows CI/CD to override with environment variables + let _ = dotenvy::dotenv(); use jsonwebtoken::{encode, EncodingKey, Header, Algorithm}; use serde::{Serialize, Deserialize}; use chrono::Utc; @@ -116,10 +119,22 @@ impl E2ETestFramework { ], }; - // Use test JWT secret (must match API Gateway config) - // CRITICAL: JWT_SECRET must be set in environment and match services + // Load JWT secret from environment (loaded from .env or CI/CD) + // CRITICAL: Must match API Gateway JWT_SECRET configuration let secret = std::env::var("JWT_SECRET") - .context("JWT_SECRET environment variable must be set for E2E tests. Run: export JWT_SECRET=")?; + .context("JWT_SECRET not configured. Options:\n \ + 1. Create .env file with JWT_SECRET (development) - AUTOMATIC\n \ + 2. Export JWT_SECRET environment variable (CI/CD)\n \ + 3. Verify .env file exists in project root")?; + + // Validate secret length (security requirement) + if secret.len() < 64 { + anyhow::bail!( + "JWT_SECRET must be at least 64 characters (current: {}). \n\ + Generate a secure secret: openssl rand -base64 64", + secret.len() + ); + } let token = encode( &Header::new(Algorithm::HS256),