#![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