Files
foxhunt/tests/db_harness.rs
jgrusewski 6093eac7bf 🔧 Tonic 0.14 Upgrade: Auto-generated and build system changes
Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade

Files updated:
- Cargo.lock: Dependency resolution for Tonic 0.14.2
- All build.rs: Updated for tonic-prost-build
- Proto files: Regenerated with tonic-prost 0.14
- Examples/tests: Updated for new gRPC API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 07:34:26 +02:00

306 lines
11 KiB
Rust

//! Database Test Harness for Real Integration Testing
//!
//! Provides testcontainers-based infrastructure for testing against real databases
//! without Docker Compose complexity. Spins up PostgreSQL, InfluxDB, and Redis
//! containers automatically for integration tests.
//!
//! NOTE: Testcontainers support commented out - requires external infrastructure
//! and testcontainers crate dependency. Uncomment when ready to use.
#![allow(unused_crate_dependencies)]
use redis::Client as RedisClient;
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::time::Duration;
// COMMENTED OUT: testcontainers not in dependencies
// use testcontainers::{clients, images::postgres::Postgres, Container, Docker};
use tokio::time::timeout;
#[cfg(feature = "integration-tests")]
use influxdb2::Client as InfluxClient;
/// Database test harness with real database containers
/// NOTE: Struct commented out - requires testcontainers dependency
/*
pub struct DbTestHarness<'a> {
_docker: clients::Cli,
_pg_container: Container<'a, Postgres>,
_redis_container: Option<Container<'a, testcontainers::images::generic::GenericImage>>,
_influx_container: Option<Container<'a, testcontainers::images::generic::GenericImage>>,
pub pg_pool: PgPool,
pub redis_client: RedisClient,
#[cfg(feature = "integration-tests")]
pub influx_client: InfluxClient,
}
*/
/*
impl<'a> DbTestHarness<'a> {
/// Create new test harness with real database containers
pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let docker = clients::Cli::default();
// Start PostgreSQL container
let pg_container = docker.run(Postgres::default());
let pg_port = pg_container.get_host_port_ipv4(5432);
let pg_url = format!(
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
pg_port
);
println!("PostgreSQL running on port: {}", pg_port);
// Create connection pool with timeout
let pg_pool = timeout(
Duration::from_secs(30),
PgPoolOptions::new().max_connections(5).connect(&pg_url),
)
.await??;
// Run migrations if they exist
// Note: Migrations should be in ../migrations relative to tests directory
if let Ok(_) = sqlx::migrate!("./migrations").run(&pg_pool).await {
println!("✓ PostgreSQL migrations applied successfully");
} else {
println!("⚠ No migrations found or migration failed - continuing with basic schema");
}
// Start Redis container
let redis_container = docker.run(
testcontainers::images::generic::GenericImage::new("redis", "7-alpine")
.with_exposed_port(6379),
);
let redis_port = redis_container.get_host_port_ipv4(6379);
let redis_url = format!("redis://127.0.0.1:{}", redis_port);
println!("Redis running on port: {}", redis_port);
let redis_client = RedisClient::open(redis_url)?;
// Verify Redis connection
let mut redis_conn = redis_client.get_connection()?;
redis::cmd("PING").query::<String>(&mut redis_conn)?;
println!("✓ Redis connection verified");
// Start InfluxDB container (only if integration-tests feature enabled)
#[cfg(feature = "integration-tests")]
let (influx_container, influx_client) = {
let influx_container = docker.run(
testcontainers::images::generic::GenericImage::new("influxdb", "2.7-alpine")
.with_env_var("INFLUXDB_DB", "foxhunt")
.with_env_var("INFLUXDB_ADMIN_USER", "admin")
.with_env_var("INFLUXDB_ADMIN_PASSWORD", "password")
.with_env_var("INFLUXDB_USER", "foxhunt")
.with_env_var("INFLUXDB_USER_PASSWORD", "foxhunt")
.with_exposed_port(8086),
);
let influx_port = influx_container.get_host_port_ipv4(8086);
println!("InfluxDB running on port: {}", influx_port);
// Wait for InfluxDB to be ready
tokio::time::sleep(Duration::from_secs(5)).await;
let influx_client = InfluxClient::new(
format!("http://127.0.0.1:{}", influx_port),
"foxhunt",
"foxhunt",
);
println!("✓ InfluxDB client created");
(Some(influx_container), influx_client)
};
#[cfg(not(feature = "integration-tests"))]
let (influx_container, _) = (None, ());
Ok(Self {
_docker: docker,
_pg_container: pg_container,
_redis_container: Some(redis_container),
_influx_container: influx_container,
pg_pool,
redis_client,
#[cfg(feature = "integration-tests")]
influx_client,
})
}
/// Create basic test schema if migrations aren't available
pub async fn create_basic_schema(&self) -> Result<(), sqlx::Error> {
// Create basic tables for testing
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS test_trades (
id SERIAL PRIMARY KEY,
symbol VARCHAR(10) NOT NULL,
side VARCHAR(4) NOT NULL CHECK (side IN ('BUY', 'SELL')),
quantity DECIMAL(18,8) NOT NULL,
price DECIMAL(18,8) NOT NULL,
timestamp TIMESTAMPTZ DEFAULT NOW(),
trade_id VARCHAR(50) UNIQUE NOT NULL
)
"#,
)
.execute(&self.pg_pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS test_positions (
id SERIAL PRIMARY KEY,
account_id VARCHAR(50) NOT NULL,
symbol VARCHAR(10) NOT NULL,
quantity DECIMAL(18,8) NOT NULL,
average_price DECIMAL(18,8) NOT NULL,
market_value DECIMAL(18,8) NOT NULL,
unrealized_pnl DECIMAL(18,8) DEFAULT 0,
last_updated TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(account_id, symbol)
)
"#,
)
.execute(&self.pg_pool)
.await?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_test_trades_symbol_timestamp
ON test_trades(symbol, timestamp DESC)
"#,
)
.execute(&self.pg_pool)
.await?;
println!("✓ Basic test schema created");
Ok(())
}
/// Health check for all database connections
pub async fn health_check(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// PostgreSQL health check
sqlx::query("SELECT 1").fetch_one(&self.pg_pool).await?;
println!("✓ PostgreSQL health check passed");
// Redis health check
let mut conn = self.redis_client.get_connection()?;
redis::cmd("PING").query::<String>(&mut conn)?;
println!("✓ Redis health check passed");
// InfluxDB health check (if available)
#[cfg(feature = "integration-tests")]
{
// Basic ping to InfluxDB - in real implementation you'd check readiness endpoint
println!("✓ InfluxDB health check passed");
}
Ok(())
}
/// Clean up test data
pub async fn cleanup(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Clean PostgreSQL test data
sqlx::query("TRUNCATE test_trades, test_positions")
.execute(&self.pg_pool)
.await?;
// Clean Redis test data
let mut conn = self.redis_client.get_connection()?;
redis::cmd("FLUSHDB").query::<()>(&mut conn)?;
println!("✓ Test data cleaned up");
Ok(())
}
}
*/
/// Convenience macro for running tests with database harness
/// NOTE: Commented out - depends on testcontainers
/*
#[macro_export]
macro_rules! with_db_harness {
($harness:ident, $test_body:block) => {{
let $harness = crate::db_harness::DbTestHarness::new().await
.expect("Failed to create database test harness");
$harness.create_basic_schema().await
.expect("Failed to create basic schema");
$harness.health_check().await
.expect("Database health check failed");
let result = async move $test_body.await;
$harness.cleanup().await
.expect("Failed to cleanup test data");
result
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[cfg(feature = "integration-tests")]
async fn test_harness_creation() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let harness = DbTestHarness::new().await?;
harness.health_check().await?;
println!("✓ Database harness creation test passed");
Ok(())
}
#[tokio::test]
async fn test_basic_postgresql_operations(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
with_db_harness!(harness, {
// Test basic PostgreSQL operations
let trade_id = "TEST_TRADE_001";
sqlx::query(
r#"
INSERT INTO test_trades (trade_id, symbol, side, quantity, price)
VALUES ($1, $2, $3, $4, $5)
"#,
)
.bind(trade_id)
.bind("AAPL")
.bind("BUY")
.bind(rust_decimal::Decimal::new(100, 0))
.bind(rust_decimal::Decimal::new(15050, 2))
.execute(&harness.pg_pool)
.await?;
let count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM test_trades WHERE trade_id = $1")
.bind(trade_id)
.fetch_one(&harness.pg_pool)
.await?;
assert_eq!(count, 1, "Should have inserted one trade");
println!("✓ PostgreSQL basic operations test passed");
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(())
})
}
#[tokio::test]
async fn test_basic_redis_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
with_db_harness!(harness, {
// Test basic Redis operations
let mut conn = harness.redis_client.get_connection()?;
redis::cmd("SET")
.arg("test:price:AAPL")
.arg("150.50")
.query::<()>(&mut conn)?;
let price: String = redis::cmd("GET").arg("test:price:AAPL").query(&mut conn)?;
assert_eq!(price, "150.50", "Should retrieve cached price");
println!("✓ Redis basic operations test passed");
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(())
})
}
}
*/