//! TDD Tests for FXT ML Trading Commands //! //! RED Phase: These tests are EXPECTED TO FAIL initially. //! The implementation will be created after these tests are written. //! //! Test Coverage: //! - `fxt trade ml submit` - Submit ML-generated order //! - `fxt trade ml predictions` - View prediction history //! - `fxt trade ml performance` - View model performance //! - Error handling for missing required arguments //! - Model filtering and limit options // Suppress false-positive unused_crate_dependencies warnings // dev-dependencies are shared across ALL test targets in the crate // This test may not use all deps, but they are required by other integration tests #![allow(unused_crate_dependencies)] #![allow(clippy::tests_outside_test_module, clippy::str_to_string, clippy::non_ascii_literal, clippy::shadow_unrelated, clippy::shadow_reuse, clippy::unwrap_used, clippy::expect_used, clippy::assertions_on_result_states, clippy::use_debug, clippy::let_underscore_must_use, clippy::string_add, clippy::string_add_assign, clippy::wildcard_enum_match_arm, clippy::unseparated_literal_suffix, clippy::indexing_slicing, clippy::doc_markdown, clippy::similar_names, clippy::impl_trait_in_params, unused_imports, dead_code, clippy::panic, unreachable_pub)] use assert_cmd::Command; use predicates::prelude::*; use serial_test::serial; use std::path::PathBuf; // ============================================================================ // Test Authentication Helper Module // ============================================================================ // // Provides test setup/teardown for JWT authentication in integration tests. // Uses real JWT token generation and FileTokenStorage for authenticity. // // ANTI-WORKAROUND COMPLIANCE: // ✅ Real JWT token generation using jsonwebtoken crate // ✅ Real FileTokenStorage (just with test tokens in temp directory) // ✅ Proper cleanup after tests complete // ❌ NO STUBS or mocks // ❌ NO PLACEHOLDERS or simplified tokens mod test_auth { use anyhow::{Context, Result}; use std::path::PathBuf; /// Setup test authentication environment /// /// Creates valid JWT tokens in a temporary directory and returns the path /// for cleanup. Tokens are valid for 1 hour. /// /// # Returns /// * `Ok(PathBuf)` - Path to temporary token directory (for cleanup) /// * `Err(anyhow::Error)` - Failed to setup authentication pub fn setup_test_auth() -> Result { use fxt::auth::jwt_generator; use fxt::auth::token_manager::{FileTokenStorage, TokenStorage}; // Create isolated temp directory for this test run let temp_dir = std::env::temp_dir().join(format!("foxhunt_fxt_test_{}", std::process::id())); // Create FileTokenStorage in temp directory let storage = FileTokenStorage::with_directory(temp_dir.clone()) .context("Failed to create FileTokenStorage for tests")?; // Generate valid access token (1 hour expiry) let (access_token, _jti) = jwt_generator::generate_access_token( "test_user", vec!["trader".to_string()], vec![ "api.access".to_string(), "trading.submit".to_string(), "trading.view".to_string(), ], 3600, // 1 hour ) .context("Failed to generate test access token")?; // Generate valid refresh token (2 hours expiry) let (refresh_token, _jti) = jwt_generator::generate_refresh_token( "test_user", 7200, // 2 hours ) .context("Failed to generate test refresh token")?; // Use tokio runtime to store tokens (FileTokenStorage is async) tokio::runtime::Runtime::new() .context("Failed to create tokio runtime")? .block_on(async { storage .store_access_token(&access_token) .await .context("Failed to store test access token")?; storage .store_refresh_token(&refresh_token) .await .context("Failed to store test refresh token")?; Ok::<(), anyhow::Error>(()) })?; println!( "✓ Test authentication setup complete in: {}", temp_dir.display() ); Ok(temp_dir) } /// Cleanup test authentication environment /// /// Removes temporary token directory and all tokens. /// /// # Arguments /// * `token_dir` - Path to temporary token directory from setup_test_auth() pub fn cleanup_test_auth(token_dir: &PathBuf) { if token_dir.exists() { if let Err(e) = std::fs::remove_dir_all(token_dir) { eprintln!("⚠ Warning: Failed to cleanup test token directory: {}", e); } else { println!("✓ Test authentication cleanup complete"); } } } /// Setup authentication with environment override /// /// Creates tokens in temporary directory and sets XDG_CONFIG_HOME to /// redirect FileTokenStorage to that temp directory. /// /// Also sets FOXHUNT_ENCRYPTION_KEY to ensure consistent encryption/decryption /// between test process and FXT binary process. /// /// This approach allows the actual FXT binary to find the test tokens /// without code modifications. /// /// # Returns /// * `Ok((PathBuf, Option, Option))` - (temp_dir, original_config_home, original_encryption_key) for cleanup pub fn setup_test_auth_with_env_override() -> Result<(PathBuf, Option, Option)> { // Save original environment variables let original_config_home = std::env::var("XDG_CONFIG_HOME").ok(); let original_encryption_key = std::env::var("FOXHUNT_ENCRYPTION_KEY").ok(); // Generate a consistent encryption key for this test run // This ensures both the test process and FXT binary use the same key let encryption_key = hex::encode([42u8; 32]); // Simple deterministic key for tests std::env::set_var("FOXHUNT_ENCRYPTION_KEY", &encryption_key); // Create temp directory structure: temp/foxhunt-fxt/tokens/ let temp_base = std::env::temp_dir().join(format!("foxhunt_fxt_config_{}", std::process::id())); let config_home = temp_base.clone(); let token_dir = config_home.join("foxhunt-fxt").join("tokens"); std::fs::create_dir_all(&token_dir) .context("Failed to create token directory structure")?; // Set XDG_CONFIG_HOME to temp directory std::env::set_var("XDG_CONFIG_HOME", &config_home); // Generate and store tokens use fxt::auth::jwt_generator; use fxt::auth::token_manager::{FileTokenStorage, TokenStorage}; let storage = FileTokenStorage::new() .context("Failed to create FileTokenStorage (should use temp XDG_CONFIG_HOME)")?; let (access_token, _) = jwt_generator::generate_access_token( "test_user", vec!["trader".to_string()], vec![ "api.access".to_string(), "trading.submit".to_string(), "trading.view".to_string(), ], 3600, )?; let (refresh_token, _) = jwt_generator::generate_refresh_token("test_user", 7200)?; tokio::runtime::Runtime::new()?.block_on(async { storage.store_access_token(&access_token).await?; storage.store_refresh_token(&refresh_token).await?; Ok::<(), anyhow::Error>(()) })?; println!( "✓ Test auth with env override: XDG_CONFIG_HOME={}, FOXHUNT_ENCRYPTION_KEY=", config_home.display() ); Ok((temp_base, original_config_home, original_encryption_key)) } /// Cleanup authentication environment override pub fn cleanup_test_auth_with_env_override( temp_base: &PathBuf, original_config_home: Option, original_encryption_key: Option, ) { // Restore original XDG_CONFIG_HOME match original_config_home { Some(original) => std::env::set_var("XDG_CONFIG_HOME", original), None => std::env::remove_var("XDG_CONFIG_HOME"), } // Restore original FOXHUNT_ENCRYPTION_KEY match original_encryption_key { Some(original) => std::env::set_var("FOXHUNT_ENCRYPTION_KEY", original), None => std::env::remove_var("FOXHUNT_ENCRYPTION_KEY"), } // Cleanup temp directory if temp_base.exists() { let _ = std::fs::remove_dir_all(temp_base); } } } /// RED TEST 1: ML order submission command /// Expected to FAIL - command doesn't exist yet #[test] #[serial] fn test_fxt_trade_ml_submit_command() { // Setup test authentication let (temp_base, original_config, original_key) = test_auth::setup_test_auth_with_env_override() .expect("Failed to setup test authentication"); let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("submit") .arg("--symbol") .arg("ES.FUT") .arg("--account") .arg("test_account"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .success() .stdout(predicate::str::contains("ML order submitted")) .stdout(predicate::str::contains("Order ID:")) .stdout(predicate::str::contains("Confidence:")); // Cleanup test_auth::cleanup_test_auth_with_env_override(&temp_base, original_config, original_key); } /// RED TEST 2: ML predictions viewing command /// Expected to FAIL - command doesn't exist yet #[serial] #[test] fn test_fxt_trade_ml_predictions_command() { // Setup test authentication let (temp_base, original_config, original_key) = test_auth::setup_test_auth_with_env_override() .expect("Failed to setup test authentication"); let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("predictions") .arg("--symbol") .arg("ES.FUT") .arg("--limit") .arg("10"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .success() .stdout(predicate::str::contains("ML Predictions for ES.FUT")) .stdout(predicate::str::contains("Predicted Action")) .stdout(predicate::str::contains("Confidence")); // Cleanup test_auth::cleanup_test_auth_with_env_override(&temp_base, original_config, original_key); } /// RED TEST 3: ML performance metrics command /// Expected to FAIL - command doesn't exist yet #[serial] #[test] fn test_fxt_trade_ml_performance_command() { // Setup test authentication let (temp_base, original_config, original_key) = test_auth::setup_test_auth_with_env_override() .expect("Failed to setup test authentication"); let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade").arg("ml").arg("performance"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .success() .stdout(predicate::str::contains("ML Model Performance")) .stdout(predicate::str::contains("Accuracy")) .stdout(predicate::str::contains("Sharpe Ratio")); // Cleanup test_auth::cleanup_test_auth_with_env_override(&temp_base, original_config, original_key); } /// RED TEST 4: ML order submission with specific model selection /// Expected to FAIL - command doesn't exist yet #[serial] #[test] fn test_fxt_trade_ml_submit_with_model_filter() { // Setup test authentication let (temp_base, original_config, original_key) = test_auth::setup_test_auth_with_env_override() .expect("Failed to setup test authentication"); let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("submit") .arg("--symbol").arg("ES.FUT") .arg("--model").arg("DQN") // Use DQN only, not ensemble .arg("--account").arg("test_account"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .success() .stdout(predicate::str::contains("Model:")) .stdout(predicate::str::contains("DQN")); // Cleanup test_auth::cleanup_test_auth_with_env_override(&temp_base, original_config, original_key); } /// RED TEST 5: ML predictions with model and limit filters /// Expected to FAIL - command doesn't exist yet #[serial] #[test] fn test_fxt_trade_ml_predictions_with_filters() { // Setup test authentication let (temp_base, original_config, original_key) = test_auth::setup_test_auth_with_env_override() .expect("Failed to setup test authentication"); let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("predictions") .arg("--symbol") .arg("ES.FUT") .arg("--model") .arg("MAMBA2") .arg("--limit") .arg("5"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .success() .stdout(predicate::str::contains("MAMBA2")); // Cleanup test_auth::cleanup_test_auth_with_env_override(&temp_base, original_config, original_key); } /// RED TEST 6: Error handling - missing required symbol argument /// Expected to FAIL - command doesn't exist yet #[test] fn test_fxt_trade_ml_submit_requires_symbol() { let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("submit") .arg("--account") .arg("test_account"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .failure() .stderr(predicate::str::contains("required").or(predicate::str::contains("symbol"))); } /// RED TEST 7: Error handling - missing required account argument /// Expected to FAIL - command doesn't exist yet #[test] fn test_fxt_trade_ml_submit_requires_account() { let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("submit") .arg("--symbol") .arg("ES.FUT"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .failure() .stderr(predicate::str::contains("required").or(predicate::str::contains("account"))); } /// RED TEST 8: ML performance with model filter /// Expected to FAIL - command doesn't exist yet #[serial] #[test] fn test_fxt_trade_ml_performance_with_model_filter() { // Setup test authentication let (temp_base, original_config, original_key) = test_auth::setup_test_auth_with_env_override() .expect("Failed to setup test authentication"); let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("performance") .arg("--model") .arg("PPO"); // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .success() .stdout(predicate::str::contains("PPO")); // Cleanup test_auth::cleanup_test_auth_with_env_override(&temp_base, original_config, original_key); } /// RED TEST 9: Ensemble mode output verification /// Expected to FAIL - command doesn't exist yet #[test] #[serial] fn test_fxt_trade_ml_submit_ensemble_mode() { // Setup test authentication let (temp_base, original_config, original_key) = test_auth::setup_test_auth_with_env_override() .expect("Failed to setup test authentication"); let mut cmd = Command::cargo_bin("fxt").unwrap(); cmd.arg("trade") .arg("ml") .arg("submit") .arg("--symbol") .arg("ES.FUT") .arg("--account") .arg("test_account"); // No --model flag = ensemble mode // This will FAIL because the command doesn't exist yet (RED phase) cmd.assert() .success() .stdout(predicate::str::contains("Ensemble")); // Cleanup test_auth::cleanup_test_auth_with_env_override(&temp_base, original_config, original_key); }