- Create comprehensive smoke test suite for post-deployment validation - Implement 4 test categories: infrastructure, service, authentication, order flow - Add graceful failure handling for unavailable services - Create automated test runner script with multiple modes (fast, verbose, category) - Document known blockers from Agent 96 (Backtesting/ML services) - Add 30+ individual smoke tests covering critical paths - Enable smoke-tests feature in tests/Cargo.toml - Create detailed README with usage and troubleshooting Test Categories: 1. Infrastructure Health: PostgreSQL, Redis, Vault, InfluxDB, Prometheus, Grafana 2. Service Health: Trading Service, API Gateway (+ blocked: Backtesting, ML) 3. Authentication Flow: JWT, sessions, revocation, rate limiting 4. Basic Order Flow: Order CRUD, positions, order history Features: - Configurable timeouts (5-10s per test) - Environment variable configuration - Graceful service unavailability handling - Parallel and sequential execution modes - Detailed pass/fail reporting Usage: ./run_smoke_tests.sh # Run all tests ./run_smoke_tests.sh --fast # Critical tests only ./run_smoke_tests.sh --verbose # Debug logging ./run_smoke_tests.sh --category infrastructure Blocked Tests (marked with #[ignore]): - Backtesting Service (config issues from Agent 96) - ML Training Service (config issues from Agent 96) Wave 125 Phase 3B - Deployment Excellence
414 lines
12 KiB
Rust
414 lines
12 KiB
Rust
//! Basic Order Flow Smoke Tests
|
|
//!
|
|
//! Tests to validate core trading functionality including order submission,
|
|
//! querying, cancellation, and position management.
|
|
|
|
use super::common::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_database_order_submission() {
|
|
let result = with_timeout(async {
|
|
let pool = sqlx::PgPool::connect(&database_url())
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Create a test order
|
|
let order_id = uuid::Uuid::new_v4();
|
|
let user_id = "smoke_test_user";
|
|
let symbol = "BTC-USD";
|
|
let order_type = "market";
|
|
let side = "buy";
|
|
let quantity = 1.0;
|
|
let price = None::<f64>;
|
|
|
|
// Insert order
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO orders (
|
|
id, user_id, symbol, order_type, side, quantity, price,
|
|
status, created_at, updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
|
"#
|
|
)
|
|
.bind(&order_id)
|
|
.bind(user_id)
|
|
.bind(symbol)
|
|
.bind(order_type)
|
|
.bind(side)
|
|
.bind(quantity)
|
|
.bind(price)
|
|
.bind("pending")
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
println!("✅ Order submitted to database");
|
|
println!(" Order ID: {}", order_id);
|
|
println!(" Symbol: {}", symbol);
|
|
println!(" Side: {}", side);
|
|
println!(" Quantity: {}", quantity);
|
|
|
|
// Cleanup
|
|
sqlx::query("DELETE FROM orders WHERE id = $1")
|
|
.bind(&order_id)
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
pool.close().await;
|
|
Ok(())
|
|
}).await;
|
|
|
|
skip_if_unavailable!("PostgreSQL", { result });
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_database_order_query() {
|
|
let result = with_timeout(async {
|
|
let pool = sqlx::PgPool::connect(&database_url())
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Create a test order
|
|
let order_id = uuid::Uuid::new_v4();
|
|
let user_id = "smoke_test_user";
|
|
let symbol = "ETH-USD";
|
|
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO orders (
|
|
id, user_id, symbol, order_type, side, quantity,
|
|
status, created_at, updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
|
"#
|
|
)
|
|
.bind(&order_id)
|
|
.bind(user_id)
|
|
.bind(symbol)
|
|
.bind("limit")
|
|
.bind("sell")
|
|
.bind(2.0)
|
|
.bind("pending")
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Query order
|
|
let order: (uuid::Uuid, String, String) = sqlx::query_as(
|
|
"SELECT id, user_id, symbol FROM orders WHERE id = $1"
|
|
)
|
|
.bind(&order_id)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
assert_eq!(order.0, order_id);
|
|
assert_eq!(order.1, user_id);
|
|
assert_eq!(order.2, symbol);
|
|
|
|
println!("✅ Order query successful");
|
|
println!(" Order ID: {}", order.0);
|
|
println!(" User ID: {}", order.1);
|
|
println!(" Symbol: {}", order.2);
|
|
|
|
// Cleanup
|
|
sqlx::query("DELETE FROM orders WHERE id = $1")
|
|
.bind(&order_id)
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
pool.close().await;
|
|
Ok(())
|
|
}).await;
|
|
|
|
skip_if_unavailable!("PostgreSQL", { result });
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_database_order_cancellation() {
|
|
let result = with_timeout(async {
|
|
let pool = sqlx::PgPool::connect(&database_url())
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Create a test order
|
|
let order_id = uuid::Uuid::new_v4();
|
|
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO orders (
|
|
id, user_id, symbol, order_type, side, quantity,
|
|
status, created_at, updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
|
"#
|
|
)
|
|
.bind(&order_id)
|
|
.bind("smoke_test_user")
|
|
.bind("BTC-USD")
|
|
.bind("limit")
|
|
.bind("buy")
|
|
.bind(0.5)
|
|
.bind("pending")
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Cancel order
|
|
sqlx::query(
|
|
"UPDATE orders SET status = $1, updated_at = NOW() WHERE id = $2"
|
|
)
|
|
.bind("cancelled")
|
|
.bind(&order_id)
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Verify cancellation
|
|
let status: (String,) = sqlx::query_as(
|
|
"SELECT status FROM orders WHERE id = $1"
|
|
)
|
|
.bind(&order_id)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
assert_eq!(status.0, "cancelled");
|
|
|
|
println!("✅ Order cancellation successful");
|
|
println!(" Order ID: {}", order_id);
|
|
println!(" Status: {}", status.0);
|
|
|
|
// Cleanup
|
|
sqlx::query("DELETE FROM orders WHERE id = $1")
|
|
.bind(&order_id)
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
pool.close().await;
|
|
Ok(())
|
|
}).await;
|
|
|
|
skip_if_unavailable!("PostgreSQL", { result });
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_database_position_management() {
|
|
let result = with_timeout(async {
|
|
let pool = sqlx::PgPool::connect(&database_url())
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
let position_id = uuid::Uuid::new_v4();
|
|
let user_id = "smoke_test_user";
|
|
let symbol = "BTC-USD";
|
|
let quantity = 1.5;
|
|
let entry_price = 50000.0;
|
|
|
|
// Create position
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO positions (
|
|
id, user_id, symbol, quantity, entry_price, current_price,
|
|
unrealized_pnl, created_at, updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
|
"#
|
|
)
|
|
.bind(&position_id)
|
|
.bind(user_id)
|
|
.bind(symbol)
|
|
.bind(quantity)
|
|
.bind(entry_price)
|
|
.bind(entry_price) // current_price = entry_price initially
|
|
.bind(0.0) // unrealized_pnl = 0 initially
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Query position
|
|
let position: (uuid::Uuid, String, f64, f64) = sqlx::query_as(
|
|
"SELECT id, symbol, quantity, entry_price FROM positions WHERE id = $1"
|
|
)
|
|
.bind(&position_id)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
assert_eq!(position.0, position_id);
|
|
assert_eq!(position.1, symbol);
|
|
assert_eq!(position.2, quantity);
|
|
assert_eq!(position.3, entry_price);
|
|
|
|
println!("✅ Position management successful");
|
|
println!(" Position ID: {}", position.0);
|
|
println!(" Symbol: {}", position.1);
|
|
println!(" Quantity: {}", position.2);
|
|
println!(" Entry Price: ${}", position.3);
|
|
|
|
// Cleanup
|
|
sqlx::query("DELETE FROM positions WHERE id = $1")
|
|
.bind(&position_id)
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
pool.close().await;
|
|
Ok(())
|
|
}).await;
|
|
|
|
skip_if_unavailable!("PostgreSQL", { result });
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_database_order_history() {
|
|
let result = with_timeout(async {
|
|
let pool = sqlx::PgPool::connect(&database_url())
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
let user_id = "smoke_test_user";
|
|
|
|
// Create multiple test orders
|
|
let order_ids: Vec<uuid::Uuid> = (0..3)
|
|
.map(|_| uuid::Uuid::new_v4())
|
|
.collect();
|
|
|
|
for order_id in &order_ids {
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO orders (
|
|
id, user_id, symbol, order_type, side, quantity,
|
|
status, created_at, updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
|
"#
|
|
)
|
|
.bind(order_id)
|
|
.bind(user_id)
|
|
.bind("BTC-USD")
|
|
.bind("market")
|
|
.bind("buy")
|
|
.bind(0.1)
|
|
.bind("filled")
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
}
|
|
|
|
// Query order history
|
|
let orders: Vec<(uuid::Uuid, String)> = sqlx::query_as(
|
|
"SELECT id, status FROM orders WHERE user_id = $1 ORDER BY created_at DESC LIMIT 10"
|
|
)
|
|
.bind(user_id)
|
|
.fetch_all(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
assert!(orders.len() >= 3, "Should find at least 3 orders");
|
|
|
|
println!("✅ Order history query successful");
|
|
println!(" Found {} orders for user {}", orders.len(), user_id);
|
|
|
|
// Cleanup
|
|
for order_id in &order_ids {
|
|
sqlx::query("DELETE FROM orders WHERE id = $1")
|
|
.bind(order_id)
|
|
.execute(&pool)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
}
|
|
|
|
pool.close().await;
|
|
Ok(())
|
|
}).await;
|
|
|
|
skip_if_unavailable!("PostgreSQL", { result });
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_complete_order_lifecycle() {
|
|
println!("🔄 Running complete order lifecycle...");
|
|
|
|
let mut steps_passed = 0;
|
|
let total_steps = 4;
|
|
|
|
if let Ok(pool) = sqlx::PgPool::connect(&database_url()).await {
|
|
let order_id = uuid::Uuid::new_v4();
|
|
|
|
// Step 1: Submit order
|
|
if sqlx::query(
|
|
r#"
|
|
INSERT INTO orders (
|
|
id, user_id, symbol, order_type, side, quantity,
|
|
status, created_at, updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
|
"#
|
|
)
|
|
.bind(&order_id)
|
|
.bind("smoke_test_user")
|
|
.bind("BTC-USD")
|
|
.bind("market")
|
|
.bind("buy")
|
|
.bind(1.0)
|
|
.bind("pending")
|
|
.execute(&pool)
|
|
.await
|
|
.is_ok() {
|
|
println!(" ✓ Order submission");
|
|
steps_passed += 1;
|
|
}
|
|
|
|
// Step 2: Query order
|
|
if sqlx::query_as::<_, (String,)>(
|
|
"SELECT status FROM orders WHERE id = $1"
|
|
)
|
|
.bind(&order_id)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.is_ok() {
|
|
println!(" ✓ Order query");
|
|
steps_passed += 1;
|
|
}
|
|
|
|
// Step 3: Update order
|
|
if sqlx::query(
|
|
"UPDATE orders SET status = $1, updated_at = NOW() WHERE id = $2"
|
|
)
|
|
.bind("filled")
|
|
.bind(&order_id)
|
|
.execute(&pool)
|
|
.await
|
|
.is_ok() {
|
|
println!(" ✓ Order update");
|
|
steps_passed += 1;
|
|
}
|
|
|
|
// Step 4: Cancel/cleanup
|
|
if sqlx::query("DELETE FROM orders WHERE id = $1")
|
|
.bind(&order_id)
|
|
.execute(&pool)
|
|
.await
|
|
.is_ok() {
|
|
println!(" ✓ Order cleanup");
|
|
steps_passed += 1;
|
|
}
|
|
|
|
pool.close().await;
|
|
} else {
|
|
println!(" ⚠ Database unavailable - skipping order lifecycle test");
|
|
return;
|
|
}
|
|
|
|
println!("\n📊 Order Lifecycle Summary:");
|
|
println!(" Steps passed: {}/{}", steps_passed, total_steps);
|
|
|
|
assert_eq!(steps_passed, total_steps, "Complete order lifecycle should pass all steps");
|
|
println!("✅ Order lifecycle check complete");
|
|
}
|