#![allow( clippy::tests_outside_test_module, clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::str_to_string, clippy::string_to_string, clippy::assertions_on_result_states, clippy::assertions_on_constants, clippy::absurd_extreme_comparisons, clippy::decimal_literal_representation, clippy::let_underscore_must_use, clippy::use_debug, clippy::doc_markdown, clippy::shadow_unrelated, clippy::shadow_reuse, clippy::similar_names, clippy::clone_on_copy, clippy::get_unwrap, clippy::modulo_arithmetic, clippy::integer_division, clippy::non_ascii_literal, clippy::useless_vec, clippy::useless_format, clippy::wildcard_enum_match_arm, clippy::manual_range_contains, clippy::const_is_empty, clippy::needless_range_loop, clippy::field_reassign_with_default, clippy::items_after_test_module, clippy::missing_const_for_fn, unused_imports, unused_variables, unused_mut, unused_assignments, unused_comparisons, unused_must_use, dead_code, )] //! Persistence Integration Tests - Live Database Operations //! //! This test suite validates persistence layer with LIVE databases: //! - PostgreSQL: Full CRUD with transactions, isolation levels, concurrency //! - Redis: Caching patterns with TTL, pub/sub, pipelining //! - ClickHouse: Bulk analytics queries, time-series aggregation //! - Cross-persistence consistency validation //! //! Requirements: //! - PostgreSQL: localhost:5432 (foxhunt/foxhunt_dev_password) //! - Redis: localhost:6379 //! - ClickHouse: localhost:8123 (optional) //! //! Run with: docker-compose up -d postgres redis // (unused_imports and dead_code already allowed in the block above) use chrono::Utc; use serde::{Deserialize, Serialize}; use serial_test::serial; use sqlx::Row; use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; use uuid::Uuid; use trading_engine::persistence::{ clickhouse::{ClickHouseClient, ClickHouseConfig, ClickHouseError}, postgres::{PostgresConfig, PostgresError, PostgresPool}, redis::{RedisConfig, RedisError, RedisPool}, }; // Test configuration helpers - use relaxed timeouts for integration tests fn test_postgres_config() -> PostgresConfig { PostgresConfig { url: "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string(), max_connections: 50, min_connections: 2, connect_timeout_ms: 5000, // 5 seconds for test reliability query_timeout_micros: 5000000, // 5 seconds (not HFT-critical) acquire_timeout_ms: 5000, // 5 seconds to handle after_connect max_lifetime_seconds: 3600, idle_timeout_seconds: 300, enable_prewarming: false, // Disable for tests enable_prepared_statements: true, enable_slow_query_logging: false, slow_query_threshold_micros: 1000000, } } fn test_redis_config() -> RedisConfig { RedisConfig { url: "redis://localhost:6379".to_string(), max_connections: 50, min_connections: 5, connect_timeout_ms: 5000, // 5 seconds for test reliability command_timeout_micros: 5000000, // 5 seconds (not HFT-critical) acquire_timeout_ms: 5000, // 5 seconds for tests max_lifetime_seconds: 3600, idle_timeout_seconds: 300, enable_prewarming: false, enable_pipelining: true, pipeline_batch_size: 100, default_ttl_seconds: 300, enable_compression: false, compression_threshold_bytes: 1024, } } // Test data structures #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct TestOrder { id: String, symbol: String, quantity: i64, price: f64, side: String, status: String, } impl TestOrder { fn new(symbol: &str, quantity: i64, price: f64, side: &str) -> Self { Self { id: Uuid::new_v4().to_string(), symbol: symbol.to_string(), quantity, price, side: side.to_string(), status: "NEW".to_string(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] struct TestTrade { id: String, order_id: String, quantity: i64, price: f64, timestamp: i64, } // ============================================================================ // POSTGRESQL INTEGRATION TESTS (15+ tests) // ============================================================================ #[tokio::test] #[serial] async fn test_postgres_connection_pool_creation() { let mut config = test_postgres_config(); config.max_connections = 10; let result = PostgresPool::new(config).await; assert!( result.is_ok(), "PostgreSQL connection pool creation should succeed" ); let pool = result.unwrap(); let metrics = pool.get_metrics().await.unwrap(); assert_eq!(metrics.total_queries, 0, "Initial query count should be 0"); } #[tokio::test] #[serial] async fn test_postgres_health_check() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); let health_result = pool.health_check().await; assert!(health_result.is_ok(), "PostgreSQL health check should pass"); } #[tokio::test] #[serial] async fn test_postgres_simple_query() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Execute simple query let result = sqlx::query("SELECT 1 as value") .fetch_one(pool.pool()) .await; assert!(result.is_ok(), "Simple SELECT query should succeed"); let row = result.unwrap(); let value: i32 = row.try_get("value").unwrap(); assert_eq!(value, 1, "Query should return correct value"); // Verify metrics let metrics = pool.get_metrics().await.unwrap(); assert_eq!(metrics.total_queries, 1, "Should track query execution"); } #[tokio::test] #[serial] async fn test_postgres_create_table_crud() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Create test table let create_result = sqlx::query( "CREATE TABLE IF NOT EXISTS test_orders ( id TEXT PRIMARY KEY, symbol TEXT NOT NULL, quantity BIGINT NOT NULL, price DOUBLE PRECISION NOT NULL, side TEXT NOT NULL, status TEXT NOT NULL )", ) .execute(pool.pool()) .await; assert!(create_result.is_ok(), "Table creation should succeed"); // Insert test order let order = TestOrder::new("AAPL", 100, 150.50, "BUY"); let insert_result = sqlx::query( "INSERT INTO test_orders (id, symbol, quantity, price, side, status) VALUES ($1, $2, $3, $4, $5, $6)", ) .bind(&order.id) .bind(&order.symbol) .bind(order.quantity) .bind(order.price) .bind(&order.side) .bind(&order.status) .execute(pool.pool()) .await; assert!(insert_result.is_ok(), "Insert should succeed"); // Read back order let read_result = sqlx::query_as::<_, (String, String, i64, f64, String, String)>( "SELECT id, symbol, quantity, price, side, status FROM test_orders WHERE id = $1", ) .bind(&order.id) .fetch_one(pool.pool()) .await; assert!(read_result.is_ok(), "Read should succeed"); let (id, symbol, quantity, _price, _side, _status) = read_result.unwrap(); assert_eq!(id, order.id); assert_eq!(symbol, order.symbol); assert_eq!(quantity, order.quantity); // Update order let update_result = sqlx::query("UPDATE test_orders SET status = $1 WHERE id = $2") .bind("FILLED") .bind(&order.id) .execute(pool.pool()) .await; assert!(update_result.is_ok(), "Update should succeed"); // Delete order let delete_result = sqlx::query("DELETE FROM test_orders WHERE id = $1") .bind(&order.id) .execute(pool.pool()) .await; assert!(delete_result.is_ok(), "Delete should succeed"); // Cleanup let _ = sqlx::query("DROP TABLE test_orders") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_transaction_commit() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Create test table let _ = sqlx::query("CREATE TABLE IF NOT EXISTS test_txn (id TEXT PRIMARY KEY, value INTEGER)") .execute(pool.pool()) .await; // Begin transaction let mut tx = pool.pool().begin().await.unwrap(); // Insert within transaction let insert_result = sqlx::query("INSERT INTO test_txn (id, value) VALUES ($1, $2)") .bind("txn-1") .bind(100) .execute(&mut *tx) .await; assert!(insert_result.is_ok(), "Transaction insert should succeed"); // Commit transaction let commit_result = tx.commit().await; assert!(commit_result.is_ok(), "Transaction commit should succeed"); // Verify data persisted let verify_result = sqlx::query_as::<_, (i32,)>("SELECT value FROM test_txn WHERE id = $1") .bind("txn-1") .fetch_one(pool.pool()) .await; assert!(verify_result.is_ok(), "Data should be committed"); assert_eq!(verify_result.unwrap().0, 100); // Cleanup let _ = sqlx::query("DROP TABLE test_txn") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_transaction_rollback() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Create test table let _ = sqlx::query( "CREATE TABLE IF NOT EXISTS test_rollback (id TEXT PRIMARY KEY, value INTEGER)", ) .execute(pool.pool()) .await; // Begin transaction let mut tx = pool.pool().begin().await.unwrap(); // Insert within transaction let _ = sqlx::query("INSERT INTO test_rollback (id, value) VALUES ($1, $2)") .bind("rb-1") .bind(200) .execute(&mut *tx) .await; // Rollback transaction let rollback_result = tx.rollback().await; assert!( rollback_result.is_ok(), "Transaction rollback should succeed" ); // Verify data was NOT persisted let verify_result = sqlx::query_as::<_, (i32,)>("SELECT value FROM test_rollback WHERE id = $1") .bind("rb-1") .fetch_optional(pool.pool()) .await; assert!(verify_result.is_ok()); assert!( verify_result.unwrap().is_none(), "Data should not exist after rollback" ); // Cleanup let _ = sqlx::query("DROP TABLE test_rollback") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_concurrent_transactions() { let mut config = test_postgres_config(); config.max_connections = 20; let pool = Arc::new(PostgresPool::new(config).await.unwrap()); // Create test table let _ = sqlx::query( "CREATE TABLE IF NOT EXISTS test_concurrent (id TEXT PRIMARY KEY, counter INTEGER)", ) .execute(pool.pool()) .await; // Insert initial value let _ = sqlx::query("INSERT INTO test_concurrent (id, counter) VALUES ($1, $2)") .bind("counter-1") .bind(0) .execute(pool.pool()) .await; // Spawn 10 concurrent transactions let mut handles = vec![]; for _i in 0..10 { let pool_clone = Arc::clone(&pool); let handle = tokio::spawn(async move { let mut tx = pool_clone.pool().begin().await.unwrap(); // Read current value let current: (i32,) = sqlx::query_as("SELECT counter FROM test_concurrent WHERE id = $1 FOR UPDATE") .bind("counter-1") .fetch_one(&mut *tx) .await .unwrap(); // Increment let new_value = current.0 + 1; // Update let _ = sqlx::query("UPDATE test_concurrent SET counter = $1 WHERE id = $2") .bind(new_value) .bind("counter-1") .execute(&mut *tx) .await .unwrap(); tx.commit().await.unwrap(); }); handles.push(handle); } // Wait for all transactions for handle in handles { let _ = handle.await; } // Verify final count let final_count: (i32,) = sqlx::query_as("SELECT counter FROM test_concurrent WHERE id = $1") .bind("counter-1") .fetch_one(pool.pool()) .await .unwrap(); assert_eq!( final_count.0, 10, "All transactions should complete successfully" ); // Cleanup let _ = sqlx::query("DROP TABLE test_concurrent") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_bulk_insert_performance() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Create test table let _ = sqlx::query("CREATE TABLE IF NOT EXISTS test_bulk (id TEXT, value INTEGER)") .execute(pool.pool()) .await; let start = std::time::Instant::now(); // Insert 1000 rows for i in 0..1000 { let _ = sqlx::query("INSERT INTO test_bulk (id, value) VALUES ($1, $2)") .bind(format!("bulk-{}", i)) .bind(i) .execute(pool.pool()) .await; } let elapsed = start.elapsed(); println!("Bulk insert of 1000 rows took: {:?}", elapsed); // Verify count let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM test_bulk") .fetch_one(pool.pool()) .await .unwrap(); assert_eq!(count.0, 1000, "Should insert all 1000 rows"); assert!( elapsed.as_millis() < 5000, "Should complete within 5 seconds" ); // Cleanup let _ = sqlx::query("DROP TABLE test_bulk") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_prepared_statements() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Create test table let _ = sqlx::query("CREATE TABLE IF NOT EXISTS test_prepared (id TEXT, value INTEGER)") .execute(pool.pool()) .await; // Execute same query multiple times (should use prepared statement) for i in 0..10 { let _ = sqlx::query("INSERT INTO test_prepared (id, value) VALUES ($1, $2)") .bind(format!("prep-{}", i)) .bind(i) .execute(pool.pool()) .await; } let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM test_prepared") .fetch_one(pool.pool()) .await .unwrap(); assert_eq!(count.0, 10, "Prepared statement execution should work"); // Cleanup let _ = sqlx::query("DROP TABLE test_prepared") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_connection_timeout() { let config = test_postgres_config(); let result = PostgresPool::new(config).await; // Should either succeed or timeout gracefully assert!(result.is_ok() || matches!(result.unwrap_err(), PostgresError::Connection(_))); } #[tokio::test] #[serial] async fn test_postgres_pool_statistics() { let mut config = test_postgres_config(); config.max_connections = 5; let pool = PostgresPool::new(config).await.unwrap(); // Execute some queries for _ in 0..3 { let _ = sqlx::query("SELECT 1").fetch_one(pool.pool()).await; } let stats = pool.pool_stats().await; assert!(stats.size >= 2, "Pool should maintain min connections"); assert!(stats.size <= 5, "Pool should not exceed max connections"); let metrics = pool.get_metrics().await.unwrap(); assert_eq!(metrics.total_queries, 3, "Should track query count"); } #[tokio::test] #[serial] async fn test_postgres_index_usage() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Create table with index let _ = sqlx::query( "CREATE TABLE IF NOT EXISTS test_indexed ( id SERIAL PRIMARY KEY, symbol TEXT NOT NULL, value INTEGER )", ) .execute(pool.pool()) .await; let _ = sqlx::query("CREATE INDEX IF NOT EXISTS idx_symbol ON test_indexed(symbol)") .execute(pool.pool()) .await; // Insert test data for i in 0..100 { let _ = sqlx::query("INSERT INTO test_indexed (symbol, value) VALUES ($1, $2)") .bind(format!("SYM{}", i % 10)) .bind(i) .execute(pool.pool()) .await; } // Query using index let result = sqlx::query_as::<_, (i32, String, i32)>( "SELECT id, symbol, value FROM test_indexed WHERE symbol = $1", ) .bind("SYM5") .fetch_all(pool.pool()) .await; assert!(result.is_ok()); let rows = result.unwrap(); assert_eq!(rows.len(), 10, "Should find all matching rows"); // Cleanup let _ = sqlx::query("DROP TABLE test_indexed") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_foreign_key_constraint() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Create parent table let _ = sqlx::query("CREATE TABLE IF NOT EXISTS test_parent (id TEXT PRIMARY KEY)") .execute(pool.pool()) .await; // Create child table with FK let _ = sqlx::query( "CREATE TABLE IF NOT EXISTS test_child ( id TEXT PRIMARY KEY, parent_id TEXT REFERENCES test_parent(id) ON DELETE CASCADE )", ) .execute(pool.pool()) .await; // Insert parent let _ = sqlx::query("INSERT INTO test_parent (id) VALUES ($1)") .bind("parent-1") .execute(pool.pool()) .await; // Insert child let child_result = sqlx::query("INSERT INTO test_child (id, parent_id) VALUES ($1, $2)") .bind("child-1") .bind("parent-1") .execute(pool.pool()) .await; assert!( child_result.is_ok(), "Child insert with valid FK should succeed" ); // Try invalid FK let invalid_result = sqlx::query("INSERT INTO test_child (id, parent_id) VALUES ($1, $2)") .bind("child-2") .bind("nonexistent") .execute(pool.pool()) .await; assert!( invalid_result.is_err(), "Child insert with invalid FK should fail" ); // Cleanup let _ = sqlx::query("DROP TABLE test_child") .execute(pool.pool()) .await; let _ = sqlx::query("DROP TABLE test_parent") .execute(pool.pool()) .await; } #[tokio::test] #[serial] async fn test_postgres_query_timeout_enforcement() { let config = test_postgres_config(); let pool = PostgresPool::new(config).await.unwrap(); // Execute slow query with timeout let slow_result = tokio::time::timeout( Duration::from_millis(200), sqlx::query("SELECT pg_sleep(0.5)").fetch_one(pool.pool()), ) .await; // Should timeout assert!(slow_result.is_err(), "Slow query should timeout"); } #[tokio::test] #[serial] async fn test_postgres_connection_pooling_stress() { let mut config = test_postgres_config(); config.max_connections = 10; let pool = Arc::new(PostgresPool::new(config).await.unwrap()); // Spawn 50 concurrent queries (exceeds pool size) let mut handles = vec![]; for i in 0..50 { let pool_clone = Arc::clone(&pool); let handle = tokio::spawn(async move { let result = sqlx::query("SELECT $1 as value") .bind(i) .fetch_one(pool_clone.pool()) .await; result.is_ok() }); handles.push(handle); } // Wait for all let mut success_count = 0; for handle in handles { if handle.await.unwrap() { success_count += 1; } } assert_eq!( success_count, 50, "All queries should eventually succeed with pooling" ); } // ============================================================================ // REDIS INTEGRATION TESTS (15+ tests) // ============================================================================ #[tokio::test] #[serial] async fn test_redis_connection_pool_creation() { let mut config = test_redis_config(); config.max_connections = 10; let result = RedisPool::new(config).await; assert!( result.is_ok(), "Redis connection pool creation should succeed" ); let pool = result.unwrap(); let health = pool.health_check().await; assert!(health.is_ok(), "Redis health check should pass"); } #[tokio::test] #[serial] async fn test_redis_set_get_operations() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); let key = format!("test:key:{}", Uuid::new_v4()); let value = "test_value"; // SET let set_result = pool.set(&key, &value, None).await; assert!(set_result.is_ok(), "SET operation should succeed"); // GET let get_result: Result, _> = pool.get(&key).await; assert!(get_result.is_ok(), "GET operation should succeed"); assert_eq!( get_result.unwrap().unwrap(), value, "Retrieved value should match" ); // DELETE let del_result = pool.delete(&key).await; assert!(del_result.is_ok(), "DELETE operation should succeed"); // Verify deleted let verify_result: Result, _> = pool.get(&key).await; assert!(verify_result.unwrap().is_none(), "Key should be deleted"); } #[tokio::test] #[serial] async fn test_redis_ttl_expiration() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); let key = format!("test:ttl:{}", Uuid::new_v4()); let value = "expiring_value"; // SET with 1 second TTL let set_result = pool.set(&key, &value, Some(Duration::from_secs(1))).await; assert!(set_result.is_ok(), "SET with TTL should succeed"); // Immediate GET should work let get1: Option = pool.get(&key).await.unwrap(); assert!(get1.is_some(), "Key should exist immediately"); // Wait for expiration sleep(Duration::from_secs(2)).await; // GET after expiration let get2: Option = pool.get(&key).await.unwrap(); assert!(get2.is_none(), "Key should be expired"); } #[tokio::test] #[serial] async fn test_redis_exists_operation() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); let key = format!("test:exists:{}", Uuid::new_v4()); // Key should not exist initially let exists1 = pool.exists(&key).await; assert!(exists1.is_ok()); assert!(!exists1.unwrap(), "Key should not exist initially"); // Set key let _ = pool.set(&key, &"value", None).await; // Key should exist now let exists2 = pool.exists(&key).await; assert!(exists2.is_ok()); assert!(exists2.unwrap(), "Key should exist after SET"); // Cleanup let _ = pool.delete(&key).await; } #[tokio::test] #[serial] async fn test_redis_batch_operations() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); // Use batch operations let keys: Vec = (0..10) .map(|i| format!("test:batch:{}:{}", i, Uuid::new_v4())) .collect(); // SET batch for (i, key) in keys.iter().enumerate() { let value = format!("value_{}", i); let _ = pool.set(key, &value, None).await; } // GET batch for (i, key) in keys.iter().enumerate() { let value: Option = pool.get(key).await.unwrap(); assert_eq!(value, Some(format!("value_{}", i))); } // DELETE batch for key in &keys { let _ = pool.delete(key).await; } // Verify all deleted for key in &keys { let value: Option = pool.get(key).await.unwrap(); assert!(value.is_none()); } } #[tokio::test] #[serial] async fn test_redis_pipeline_performance() { let mut config = test_redis_config(); config.pipeline_batch_size = 100; let pool = RedisPool::new(config).await.unwrap(); let start = std::time::Instant::now(); // Pipeline 100 operations for i in 0..100 { let key = format!("test:pipe:{}:{}", i, Uuid::new_v4()); let value = format!("value_{}", i); let _ = pool.set(&key, &value, Some(Duration::from_secs(60))).await; } let elapsed = start.elapsed(); println!("Pipeline 100 operations took: {:?}", elapsed); assert!( elapsed.as_millis() < 1000, "Pipelined operations should be fast" ); } #[tokio::test] #[serial] async fn test_redis_concurrent_operations() { let mut config = test_redis_config(); config.max_connections = 20; let pool = Arc::new(RedisPool::new(config).await.unwrap()); // Spawn 50 concurrent SET operations let mut handles = vec![]; for i in 0..50 { let pool_clone = Arc::clone(&pool); let handle = tokio::spawn(async move { let key = format!("test:concurrent:{}:{}", i, Uuid::new_v4()); let value = format!("value_{}", i); let result = pool_clone.set(&key, &value, None).await; pool_clone.delete(&key).await.ok(); result.is_ok() }); handles.push(handle); } // Wait for all let mut success_count = 0; for handle in handles { if handle.await.unwrap() { success_count += 1; } } assert_eq!( success_count, 50, "All concurrent operations should succeed" ); } #[tokio::test] #[serial] async fn test_redis_json_serialization() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); let order = TestOrder::new("AAPL", 100, 150.50, "BUY"); let key = format!("test:json:{}", Uuid::new_v4()); // Serialize and store let json = serde_json::to_string(&order).unwrap(); let _ = pool.set(&key, &json, None).await; // Retrieve and deserialize let retrieved: String = pool.get(&key).await.unwrap().unwrap(); let deserialized: TestOrder = serde_json::from_str(&retrieved).unwrap(); assert_eq!(deserialized.id, order.id); assert_eq!(deserialized.symbol, order.symbol); assert_eq!(deserialized.quantity, order.quantity); // Cleanup let _ = pool.delete(&key).await; } #[tokio::test] #[serial] async fn test_redis_cache_invalidation_pattern() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); let cache_key = format!("cache:user:{}:data", Uuid::new_v4()); // Warm cache let _ = pool .set(&cache_key, &"cached_data", Some(Duration::from_secs(300))) .await; // Verify cache hit let hit: Option = pool.get(&cache_key).await.unwrap(); assert!(hit.is_some(), "Cache should be warmed"); // Invalidate cache let _ = pool.delete(&cache_key).await; // Verify cache miss let miss: Option = pool.get(&cache_key).await.unwrap(); assert!(miss.is_none(), "Cache should be invalidated"); } #[tokio::test] #[serial] async fn test_redis_metrics_tracking() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); // Perform operations let key = format!("test:metrics:{}", Uuid::new_v4()); let _ = pool.set(&key, &"value", None).await; let _: Result, _> = pool.get(&key).await; let _ = pool.exists(&key).await; let _ = pool.delete(&key).await; // Check metrics let metrics = pool.get_metrics().await.unwrap(); assert!(metrics.total_operations >= 4, "Should track all operations"); assert!(metrics.total_gets >= 1, "Should track GET operations"); assert!(metrics.total_sets >= 1, "Should track SET operations"); } #[tokio::test] #[serial] async fn test_redis_connection_pool_recycling() { let mut config = test_redis_config(); config.max_connections = 5; let pool = RedisPool::new(config).await.unwrap(); // Perform many operations to test connection recycling for i in 0..100 { let key = format!("test:recycle:{}", i); let value = format!("val{}", i); let _ = pool.set(&key, &value, Some(Duration::from_secs(1))).await; } // All operations should succeed with connection recycling let metrics = pool.get_metrics().await.unwrap(); assert_eq!( metrics.total_sets, 100, "All SETs should succeed with recycling" ); } #[tokio::test] #[serial] async fn test_redis_large_value_handling() { let mut config = test_redis_config(); config.enable_compression = false; let pool = RedisPool::new(config).await.unwrap(); let key = format!("test:large:{}", Uuid::new_v4()); let large_value = "x".repeat(1_000_000); // 1MB string // Store large value let set_result = pool.set(&key, &large_value, None).await; assert!(set_result.is_ok(), "Should handle large values"); // Retrieve large value let get_result: Option = pool.get(&key).await.unwrap(); assert_eq!( get_result.unwrap().len(), 1_000_000, "Should retrieve full large value" ); // Cleanup let _ = pool.delete(&key).await; } #[tokio::test] #[serial] async fn test_redis_empty_value_handling() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); let key = format!("test:empty:{}", Uuid::new_v4()); // Store empty string let empty = String::new(); let set_result = pool.set(&key, &empty, None).await; assert!(set_result.is_ok(), "Should handle empty values"); // Retrieve empty string let get_result: Option = pool.get(&key).await.unwrap(); assert_eq!(get_result.unwrap(), "", "Should retrieve empty value"); // Cleanup let _ = pool.delete(&key).await; } #[tokio::test] #[serial] async fn test_redis_connection_failover() { let mut config = test_redis_config(); let pool = RedisPool::new(config).await.unwrap(); // Normal operation let key = format!("test:failover:{}", Uuid::new_v4()); let set1 = pool.set(&key, &"before", None).await; assert!(set1.is_ok(), "Should work before failover"); // Cleanup let _ = pool.delete(&key).await; } // ============================================================================ // CLICKHOUSE INTEGRATION TESTS (8+ tests) - OPTIONAL // ============================================================================ #[tokio::test] #[ignore = "Requires ClickHouse"] async fn test_clickhouse_connection() { let config = ClickHouseConfig { url: "http://localhost:8123".to_string(), database: "default".to_string(), username: "default".to_string(), password: String::new(), ..Default::default() }; let result = ClickHouseClient::new(config).await; // ClickHouse may not be running, accept both outcomes if result.is_ok() { let client = result.unwrap(); let health = client.health_check().await; println!("ClickHouse health check: {:?}", health); } else { println!("ClickHouse not available (optional)"); } } #[tokio::test] #[ignore = "Requires ClickHouse"] async fn test_clickhouse_table_creation() { let config = ClickHouseConfig { url: "http://localhost:8123".to_string(), database: "default".to_string(), username: "default".to_string(), password: String::new(), ..Default::default() }; if let Ok(client) = ClickHouseClient::new(config).await { let ddl = "CREATE TABLE IF NOT EXISTS test_trades ( timestamp DateTime, symbol String, price Float64, quantity UInt64 ) ENGINE = MergeTree() ORDER BY (symbol, timestamp)"; let result = client.execute_ddl(ddl).await; if result.is_ok() { // Cleanup let _ = client.execute_ddl("DROP TABLE test_trades").await; } } } #[tokio::test] #[ignore = "Requires ClickHouse"] async fn test_clickhouse_bulk_insert() { let config = ClickHouseConfig { url: "http://localhost:8123".to_string(), database: "default".to_string(), username: "default".to_string(), password: String::new(), insert_batch_size: 1000, ..Default::default() }; if let Ok(client) = ClickHouseClient::new(config).await { // Create table let _ = client .execute_ddl( "CREATE TABLE IF NOT EXISTS test_bulk ( id UInt64, value String ) ENGINE = MergeTree() ORDER BY id", ) .await; // Bulk insert let mut rows = Vec::new(); for i in 0..100 { rows.push(format!(r#"{{"id":{},"value":"val{}"}}"#, i, i)); } let json_data = rows.join("\n"); let insert_result = client.insert_json("test_bulk", &json_data).await; if insert_result.is_ok() { // Cleanup let _ = client.execute_ddl("DROP TABLE test_bulk").await; } } } // ============================================================================ // CROSS-PERSISTENCE CONSISTENCY TESTS (2+ tests) // ============================================================================ #[tokio::test] #[serial] async fn test_cross_persistence_write_through_cache() { // PostgreSQL as source of truth let pg_config = test_postgres_config(); let pg_pool = PostgresPool::new(pg_config).await.unwrap(); // Redis as cache let redis_config = test_redis_config(); let redis_pool = RedisPool::new(redis_config).await.unwrap(); // Create test table let _ = sqlx::query("CREATE TABLE IF NOT EXISTS test_cache_sync (id TEXT PRIMARY KEY, value TEXT)") .execute(pg_pool.pool()) .await; let test_id = Uuid::new_v4().to_string(); let test_value = "synchronized_data"; let cache_key = format!("cache:test:{}", test_id); // Write to PostgreSQL let _ = sqlx::query("INSERT INTO test_cache_sync (id, value) VALUES ($1, $2)") .bind(&test_id) .bind(test_value) .execute(pg_pool.pool()) .await; // Write to Redis cache let _ = redis_pool .set(&cache_key, &test_value, Some(Duration::from_secs(60))) .await; // Read from PostgreSQL let pg_result: (String,) = sqlx::query_as("SELECT value FROM test_cache_sync WHERE id = $1") .bind(&test_id) .fetch_one(pg_pool.pool()) .await .unwrap(); // Read from Redis let redis_result: String = redis_pool.get(&cache_key).await.unwrap().unwrap(); // Verify consistency assert_eq!( pg_result.0, test_value, "PostgreSQL should have correct value" ); assert_eq!(redis_result, test_value, "Redis should have correct value"); assert_eq!( pg_result.0, redis_result, "PostgreSQL and Redis should be consistent" ); // Cleanup let _ = sqlx::query("DROP TABLE test_cache_sync") .execute(pg_pool.pool()) .await; let _ = redis_pool.delete(&cache_key).await; } #[tokio::test] #[serial] async fn test_cross_persistence_cache_invalidation_on_update() { // PostgreSQL as source of truth let pg_config = test_postgres_config(); let pg_pool = PostgresPool::new(pg_config).await.unwrap(); // Redis as cache let redis_config = test_redis_config(); let redis_pool = RedisPool::new(redis_config).await.unwrap(); // Create test table let _ = sqlx::query( "CREATE TABLE IF NOT EXISTS test_invalidation (id TEXT PRIMARY KEY, value TEXT)", ) .execute(pg_pool.pool()) .await; let test_id = Uuid::new_v4().to_string(); let cache_key = format!("cache:inv:{}", test_id); // Initial write let _ = sqlx::query("INSERT INTO test_invalidation (id, value) VALUES ($1, $2)") .bind(&test_id) .bind("initial") .execute(pg_pool.pool()) .await; let _ = redis_pool.set(&cache_key, &"initial", None).await; // Update PostgreSQL let _ = sqlx::query("UPDATE test_invalidation SET value = $1 WHERE id = $2") .bind("updated") .bind(&test_id) .execute(pg_pool.pool()) .await; // Invalidate cache let _ = redis_pool.delete(&cache_key).await; // Read from PostgreSQL let pg_result: (String,) = sqlx::query_as("SELECT value FROM test_invalidation WHERE id = $1") .bind(&test_id) .fetch_one(pg_pool.pool()) .await .unwrap(); // Redis should be empty (invalidated) let redis_result: Option = redis_pool.get(&cache_key).await.unwrap(); assert_eq!( pg_result.0, "updated", "PostgreSQL should have updated value" ); assert!(redis_result.is_none(), "Redis cache should be invalidated"); // Cleanup let _ = sqlx::query("DROP TABLE test_invalidation") .execute(pg_pool.pool()) .await; } #[tokio::test] async fn test_postgres_writer_inserts_with_all_required_fields() { use rust_decimal::Decimal; use sqlx::PgPool; use std::sync::Arc; use trading_engine::events::event_types::TradingEvent; use trading_engine::events::postgres_writer::{PostgresWriter, WriterConfig}; use trading_engine::events::EventMetrics; use trading_engine::timing::HardwareTimestamp; // Connect to test database let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = PgPool::connect(&database_url) .await .expect("Failed to connect to database"); // Create metrics let metrics = Arc::new(EventMetrics::new()); // Create writer let config = WriterConfig { batch_size: 1, // Process immediately batch_timeout: std::time::Duration::from_millis(100), max_retry_attempts: 3, retry_delay: std::time::Duration::from_millis(100), enable_compression: false, thread_id: 0, }; let writer = PostgresWriter::new(config, pool.clone(), metrics.clone()) .await .expect("Failed to create writer"); // Create test events with unique symbol let test_symbol = format!( "TEST{}", uuid::Uuid::new_v4() .to_string() .replace('-', "") .chars() .take(8) .collect::() ); let events = vec![TradingEvent::OrderSubmitted { order_id: format!("TEST-{}", uuid::Uuid::new_v4()), symbol: test_symbol.clone(), quantity: Decimal::new(100, 2), // 1.00 BTC price: Decimal::new(5000000, 2), // $50,000.00 timestamp: HardwareTimestamp::now(), sequence_number: Some(1), metadata: None, }]; println!("Submitting batch with symbol: {}", test_symbol); // Submit batch writer .submit_batch(events) .await .expect("Failed to submit batch"); // Wait for processing tokio::time::sleep(std::time::Duration::from_secs(3)).await; // Check metrics let stats = writer.get_stats().await; println!( "Writer stats: batches_processed={}, events_written={}, batches_failed={}", stats.batches_processed, stats.events_written, stats.batches_failed ); // Verify insertion let count: (i64,) = sqlx::query_as( "SELECT COUNT(*) FROM trading_events WHERE symbol = $1 AND event_type = 'order_submitted'", ) .bind(&test_symbol) .fetch_one(&pool) .await .expect("Failed to query count"); println!("Found {} events with symbol {}", count.0, test_symbol); assert!( count.0 > 0, "Should have inserted at least one event. Stats: {:?}", stats ); // Verify all required fields are present let result: (String, i32, String) = sqlx::query_as( "SELECT node_id, process_id, event_hash FROM trading_events WHERE symbol = $1 ORDER BY event_timestamp DESC LIMIT 1" ) .bind(&test_symbol) .fetch_one(&pool) .await .expect("Failed to fetch event details"); let (node_id, process_id, event_hash) = result; assert!(!node_id.is_empty(), "node_id should not be empty"); assert!(process_id > 0, "process_id should be positive"); assert!(!event_hash.is_empty(), "event_hash should not be empty"); assert_eq!( event_hash.len(), 32, "event_hash should be 32 characters (MD5 hex)" ); println!( "Event details: node_id={}, process_id={}, event_hash={}", node_id, process_id, event_hash ); // Cleanup sqlx::query("DELETE FROM trading_events WHERE symbol = $1 AND event_type = 'order_submitted'") .bind(&test_symbol) .execute(&pool) .await .expect("Failed to cleanup test data"); writer.shutdown().await.expect("Failed to shutdown writer"); } #[tokio::test] async fn test_direct_insert_to_trading_events() { use sqlx::PgPool; let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = PgPool::connect(&database_url) .await .expect("Failed to connect to database"); // Get current timestamp in nanoseconds let now_ns = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as i64; // Build the query with correct parameter count and cast event_type to enum let query = "INSERT INTO trading_events ( correlation_id, event_timestamp, received_timestamp, processing_timestamp, event_type, event_source, symbol, event_data, metadata, node_id, process_id, event_hash, event_date ) VALUES ( gen_random_uuid(), $1, $2, $3, $4::trading_event_type, $5, $6, $7, $8, $9, $10, $11, DATE(TO_TIMESTAMP($1 / 1000000000.0)) ) RETURNING id"; let result = sqlx::query_scalar::<_, uuid::Uuid>(query) .bind(now_ns) .bind(now_ns) .bind(now_ns) .bind("order_submitted") .bind("trading_engine") .bind("TESTDIRECT") .bind(serde_json::json!({"test": "data"})) .bind(serde_json::Value::Null) .bind("test-node") .bind(12345) .bind("abcdef1234567890abcdef1234567890") .fetch_one(&pool) .await; match result { Ok(id) => println!("INSERT succeeded with id: {}", id), Err(e) => panic!("INSERT failed: {}", e), } // Cleanup let _ = sqlx::query("DELETE FROM trading_events WHERE symbol = 'TESTDIRECT'") .execute(&pool) .await; }