//! Comprehensive Migration Tests for Database Schema Evolution //! //! Tests cover: //! - Migration metadata verification //! - Sequential migration ordering //! - Schema evolution (columns, indexes, constraints) //! - Foreign key and check constraints //! - Index creation and verification //! - Extension and custom type verification //! - Complete schema validation //! - Data preservation verification //! //! Test Database Setup: //! ```bash //! docker-compose up -d postgres //! cargo sqlx migrate run //! ``` //! //! Note: Tests use the existing foxhunt database with applied migrations. use sqlx::postgres::PgPoolOptions; use sqlx::{PgPool, Row}; use std::time::Instant; use uuid::Uuid; const DATABASE_URL: &str = "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt"; /// Helper to get database pool async fn get_pool() -> Result { PgPoolOptions::new() .max_connections(5) .connect(DATABASE_URL) .await } /// Helper to count applied migrations async fn count_migrations(pool: &PgPool) -> Result { let row = sqlx::query("SELECT COUNT(*) as count FROM _sqlx_migrations") .fetch_one(pool) .await?; Ok(row.get("count")) } /// Helper to get migration versions async fn get_migration_versions(pool: &PgPool) -> Result, sqlx::Error> { let rows = sqlx::query("SELECT version FROM _sqlx_migrations ORDER BY version") .fetch_all(pool) .await?; Ok(rows.iter().map(|r| r.get("version")).collect()) } /// Helper to check if table exists async fn table_exists(pool: &PgPool, table_name: &str) -> Result { let row = sqlx::query( "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1) as exists", ) .bind(table_name) .fetch_one(pool) .await?; Ok(row.get("exists")) } /// Helper to get table columns async fn get_columns(pool: &PgPool, table_name: &str) -> Result, sqlx::Error> { let rows = sqlx::query( "SELECT column_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position", ) .bind(table_name) .fetch_all(pool) .await?; Ok(rows.iter().map(|r| r.get("column_name")).collect()) } /// Helper to check if index exists async fn index_exists(pool: &PgPool, index_name: &str) -> Result { let row = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = $1) as exists") .bind(index_name) .fetch_one(pool) .await?; Ok(row.get("exists")) } // ============================================================================ // Migration Metadata Tests // ============================================================================ #[tokio::test] async fn test_migrations_table_exists() { let pool = get_pool().await.expect("Failed to connect"); assert!(table_exists(&pool, "_sqlx_migrations").await.unwrap()); println!("✅ Migration tracking table exists"); pool.close().await; } #[tokio::test] async fn test_migrations_applied() { let pool = get_pool().await.expect("Failed to connect"); let count = count_migrations(&pool).await.unwrap(); assert!(count > 0, "Expected migrations, got {}", count); println!("✅ {} migrations applied", count); pool.close().await; } #[tokio::test] async fn test_migration_sequential_ordering() { let pool = get_pool().await.expect("Failed to connect"); let versions = get_migration_versions(&pool).await.unwrap(); for i in 1..versions.len() { assert!( versions[i] > versions[i - 1], "Migrations not sequential: {} > {}", versions[i], versions[i - 1] ); } println!("✅ {} migrations in sequential order", versions.len()); pool.close().await; } #[tokio::test] async fn test_all_migrations_succeeded() { let pool = get_pool().await.expect("Failed to connect"); let failed: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM _sqlx_migrations WHERE success = false") .fetch_one(&pool) .await .unwrap(); assert_eq!(failed, 0, "Found {} failed migrations", failed); println!("✅ All migrations succeeded"); pool.close().await; } #[tokio::test] async fn test_migration_timestamps() { let pool = get_pool().await.expect("Failed to connect"); let rows = sqlx::query("SELECT version, installed_on FROM _sqlx_migrations ORDER BY version") .fetch_all(&pool) .await .unwrap(); assert!(!rows.is_empty(), "No migrations found"); println!("✅ {} migrations have timestamps", rows.len()); pool.close().await; } // ============================================================================ // Schema Verification Tests // ============================================================================ #[tokio::test] async fn test_trading_events_table() { let pool = get_pool().await.expect("Failed to connect"); assert!(table_exists(&pool, "trading_events").await.unwrap()); let cols = get_columns(&pool, "trading_events").await.unwrap(); assert!(cols.contains(&"id".to_string())); assert!(cols.contains(&"event_type".to_string())); assert!(cols.contains(&"event_timestamp".to_string())); println!("✅ trading_events table verified: {} columns", cols.len()); pool.close().await; } #[tokio::test] async fn test_risk_events_table() { let pool = get_pool().await.expect("Failed to connect"); assert!(table_exists(&pool, "risk_events").await.unwrap()); let cols = get_columns(&pool, "risk_events").await.unwrap(); assert!(cols.contains(&"id".to_string())); assert!(cols.contains(&"event_type".to_string())); println!("✅ risk_events table verified: {} columns", cols.len()); pool.close().await; } #[tokio::test] async fn test_audit_log_table() { let pool = get_pool().await.expect("Failed to connect"); assert!(table_exists(&pool, "audit_log").await.unwrap()); println!("✅ audit_log table verified"); pool.close().await; } #[tokio::test] async fn test_users_table() { let pool = get_pool().await.expect("Failed to connect"); assert!(table_exists(&pool, "users").await.unwrap()); let cols = get_columns(&pool, "users").await.unwrap(); assert!(cols.contains(&"id".to_string())); assert!(cols.contains(&"username".to_string())); assert!(cols.contains(&"email".to_string())); assert!(cols.contains(&"password_hash".to_string())); println!("✅ users table verified: {} columns", cols.len()); pool.close().await; } #[tokio::test] async fn test_executions_table() { let pool = get_pool().await.expect("Failed to connect"); assert!(table_exists(&pool, "executions").await.unwrap()); let cols = get_columns(&pool, "executions").await.unwrap(); assert!(cols.contains(&"id".to_string())); assert!(cols.contains(&"order_id".to_string())); assert!(cols.contains(&"account_id".to_string())); assert!(cols.contains(&"symbol".to_string())); println!("✅ executions table verified: {} columns", cols.len()); pool.close().await; } // ============================================================================ // Index Verification Tests // ============================================================================ #[tokio::test] async fn test_executions_indexes() { let pool = get_pool().await.expect("Failed to connect"); assert!(index_exists(&pool, "idx_executions_account_id") .await .unwrap()); assert!(index_exists(&pool, "idx_executions_order_id") .await .unwrap()); println!("✅ Executions table indexes verified"); pool.close().await; } #[tokio::test] async fn test_index_count() { let pool = get_pool().await.expect("Failed to connect"); let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM pg_indexes WHERE schemaname = 'public'") .fetch_one(&pool) .await .unwrap(); assert!(count >= 5, "Expected at least 5 indexes, got {}", count); println!("✅ {} indexes found", count); pool.close().await; } // ============================================================================ // Constraint Tests // ============================================================================ #[tokio::test] async fn test_check_constraint_negative_quantity() { let pool = get_pool().await.expect("Failed to connect"); if !table_exists(&pool, "executions").await.unwrap() { println!("⚠️ Skipping: executions table doesn't exist"); return; } let result = sqlx::query( "INSERT INTO executions (order_id, account_id, symbol, side, quantity, price) VALUES ($1, $2, $3, $4, $5, $6)", ) .bind(Uuid::new_v4()) .bind("test") .bind("TEST") .bind("buy") .bind(-100_i64) .bind(1000_i64) .execute(&pool) .await; assert!( result.is_err(), "Check constraint should reject negative quantity" ); println!("✅ Check constraint verified"); pool.close().await; } #[tokio::test] async fn test_unique_constraint_username() { let pool = get_pool().await.expect("Failed to connect"); let username = format!("test_user_{}", Uuid::new_v4().simple()); // First insert should succeed let result1 = sqlx::query( "INSERT INTO users (username, email, password_hash, salt) VALUES ($1, $2, $3, $4)", ) .bind(&username) .bind(format!("{}@test.com", username)) .bind("hash") .bind("salt") .execute(&pool) .await; if result1.is_ok() { // Second insert should fail let result2 = sqlx::query( "INSERT INTO users (username, email, password_hash, salt) VALUES ($1, $2, $3, $4)", ) .bind(&username) .bind(format!("{}2@test.com", username)) .bind("hash") .bind("salt") .execute(&pool) .await; assert!( result2.is_err(), "Unique constraint should reject duplicate" ); println!("✅ Unique constraint verified"); // Cleanup sqlx::query("DELETE FROM users WHERE username = $1") .bind(&username) .execute(&pool) .await .ok(); } else { println!("⚠️ Skipping: users table not writable"); } pool.close().await; } // ============================================================================ // Extension Tests // ============================================================================ #[tokio::test] async fn test_uuid_extension() { let pool = get_pool().await.expect("Failed to connect"); let extensions: Vec = sqlx::query_scalar("SELECT extname FROM pg_extension WHERE extname = 'uuid-ossp'") .fetch_all(&pool) .await .unwrap(); assert!( !extensions.is_empty(), "uuid-ossp extension should be installed" ); println!("✅ uuid-ossp extension installed"); pool.close().await; } #[tokio::test] async fn test_custom_types() { let pool = get_pool().await.expect("Failed to connect"); let types: Vec = sqlx::query_scalar( "SELECT typname FROM pg_type WHERE typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') AND typtype = 'e'", ) .fetch_all(&pool) .await .unwrap(); assert!(!types.is_empty(), "Should have custom enum types"); println!("✅ {} custom types found", types.len()); pool.close().await; } // ============================================================================ // Data Tests // ============================================================================ #[tokio::test] async fn test_insert_and_query_execution() { let pool = get_pool().await.expect("Failed to connect"); if !table_exists(&pool, "executions").await.unwrap() { println!("⚠️ Skipping: executions table doesn't exist"); return; } let test_id = Uuid::new_v4(); // Insert test record let result = sqlx::query( "INSERT INTO executions (id, order_id, account_id, symbol, side, quantity, price) VALUES ($1, $2, $3, $4, $5, $6, $7)", ) .bind(test_id) .bind(Uuid::new_v4()) .bind("test_account") .bind("TESTBTC") .bind("buy") .bind(100_i64) .bind(50000_i64) .execute(&pool) .await; if result.is_ok() { // Query it back let found: Option<(Uuid,)> = sqlx::query_as("SELECT id FROM executions WHERE id = $1") .bind(test_id) .fetch_optional(&pool) .await .unwrap(); assert!(found.is_some(), "Should find inserted record"); println!("✅ Data insertion and retrieval verified"); // Cleanup sqlx::query("DELETE FROM executions WHERE id = $1") .bind(test_id) .execute(&pool) .await .ok(); } else { println!("⚠️ Skipping: executions table not writable"); } pool.close().await; } #[tokio::test] async fn test_bulk_insert_performance() { let pool = get_pool().await.expect("Failed to connect"); if !table_exists(&pool, "executions").await.unwrap() { println!("⚠️ Skipping: executions table doesn't exist"); return; } let start = Instant::now(); let batch_size = 100; let test_symbol = format!( "TEST{}", Uuid::new_v4() .simple() .to_string() .chars() .take(6) .collect::() ); for _ in 0..batch_size { let result = sqlx::query( "INSERT INTO executions (order_id, account_id, symbol, side, quantity, price) VALUES ($1, $2, $3, $4, $5, $6)", ) .bind(Uuid::new_v4()) .bind("test_account") .bind(&test_symbol) .bind("buy") .bind(100_i64) .bind(50000_i64) .execute(&pool) .await; if result.is_err() { println!("⚠️ Skipping: executions table not writable"); pool.close().await; return; } } let duration = start.elapsed(); let throughput = batch_size as f64 / duration.as_secs_f64(); println!( "✅ Bulk insert: {} rows in {:?} ({:.0} rows/sec)", batch_size, duration, throughput ); // Cleanup sqlx::query("DELETE FROM executions WHERE symbol = $1") .bind(&test_symbol) .execute(&pool) .await .ok(); pool.close().await; } // ============================================================================ // Schema Coverage Tests // ============================================================================ #[tokio::test] async fn test_all_tables() { let pool = get_pool().await.expect("Failed to connect"); let tables: Vec = sqlx::query_scalar( "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename", ) .fetch_all(&pool) .await .unwrap(); assert!(!tables.is_empty(), "Should have tables"); println!("✅ {} tables found:", tables.len()); for table in tables.iter().take(20) { println!(" - {}", table); } pool.close().await; } #[tokio::test] async fn test_all_views() { let pool = get_pool().await.expect("Failed to connect"); let views: Vec = sqlx::query_scalar( "SELECT viewname FROM pg_views WHERE schemaname = 'public' ORDER BY viewname", ) .fetch_all(&pool) .await .unwrap(); println!("✅ {} views found", views.len()); pool.close().await; } #[tokio::test] async fn test_all_functions() { let pool = get_pool().await.expect("Failed to connect"); let functions: Vec = sqlx::query_scalar( "SELECT proname FROM pg_proc WHERE pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') ORDER BY proname", ) .fetch_all(&pool) .await .unwrap(); println!("✅ {} functions found", functions.len()); pool.close().await; } #[tokio::test] async fn test_migration_file_count() { let migration_files = std::fs::read_dir("./migrations") .expect("Failed to read migrations directory") .filter_map(|e| e.ok()) .filter(|e| { let name = e.file_name(); let name_str = name.to_str().unwrap(); e.path().extension().and_then(|s| s.to_str()) == Some("sql") && !name_str.contains("backup") && !name_str.contains("broken") }) .count(); assert!(migration_files > 0, "Should have migration files"); println!("✅ {} migration files found", migration_files); }