diff --git a/backend/crates/integration-tests/src/api_postgres_tests.rs b/backend/crates/integration-tests/src/api_postgres_tests.rs index 776cc3a..94e4d03 100644 --- a/backend/crates/integration-tests/src/api_postgres_tests.rs +++ b/backend/crates/integration-tests/src/api_postgres_tests.rs @@ -17,9 +17,6 @@ use axum::{ use domain::{User, Product, CreateUser, UpdateUser, CreateProduct, UpdateProduct}; use postgres::{PostgresUserRepository, PostgresProductRepository}; use application::Service; -use sqlx::PgPool; -use sqlx::postgres::PgPoolOptions; -use std::env; use std::sync::Arc; use tower::ServiceExt; use tower_http::trace::TraceLayer; @@ -30,6 +27,9 @@ use axum::response::IntoResponse; use serial_test::serial; use application::UseCase; +// Import the centralized test setup +use crate::test_setup::{setup_test_db, unique_test_data}; + /// Application state containing user and product services. pub struct AppState { user_service: Arc, @@ -49,34 +49,8 @@ where } } -// Helper functions -pub async fn setup_test_db() -> PgPool { - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to connect to test database"); - sqlx::migrate!("../../migrations") - .run(&pool) - .await - .expect("Failed to run migrations"); - cleanup_test_data(&pool).await; - pool -} - -pub async fn cleanup_test_data(pool: &PgPool) { - let mut tx = pool.begin().await.expect("Failed to begin transaction"); - sqlx::query("DELETE FROM products").execute(&mut *tx).await.expect("Failed to delete products"); - sqlx::query("DELETE FROM users").execute(&mut *tx).await.expect("Failed to delete users"); - tx.commit().await.expect("Failed to commit cleanup transaction"); -} - -pub fn unique_test_data(prefix: &str) -> (String, String) { - let id = Uuid::new_v4().to_string()[..8].to_string(); - (format!("{}_{}", prefix, id), format!("{}_test@example.com", prefix)) -} +// Helper functions - now using centralized setup +// These functions are now imported from test_setup module above pub async fn create_test_app() -> Router { let pool = setup_test_db().await; diff --git a/backend/crates/integration-tests/src/cli_tests.rs b/backend/crates/integration-tests/src/cli_tests.rs index 330eff3..dd6a3b1 100644 --- a/backend/crates/integration-tests/src/cli_tests.rs +++ b/backend/crates/integration-tests/src/cli_tests.rs @@ -15,41 +15,14 @@ use cli::Cli; use application::Service; use memory::{InMemoryUserRepository, InMemoryProductRepository}; use postgres::{PostgresUserRepository, PostgresProductRepository}; -use sqlx::PgPool; -use sqlx::postgres::PgPoolOptions; -use std::env; use uuid::Uuid; use serial_test::serial; use application::UseCase; -// Helper functions for test setup -async fn setup_test_db() -> PgPool { - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to connect to test database"); - sqlx::migrate!("../../migrations") - .run(&pool) - .await - .expect("Failed to run migrations"); - cleanup_test_data(&pool).await; - pool -} +// Import the centralized test setup +use crate::test_setup::{setup_test_db, unique_test_data}; -async fn cleanup_test_data(pool: &PgPool) { - let mut tx = pool.begin().await.expect("Failed to begin transaction"); - sqlx::query("DELETE FROM products").execute(&mut *tx).await.expect("Failed to delete products"); - sqlx::query("DELETE FROM users").execute(&mut *tx).await.expect("Failed to delete users"); - tx.commit().await.expect("Failed to commit cleanup transaction"); -} - -fn unique_test_data(prefix: &str) -> (String, String) { - let id = Uuid::new_v4().to_string()[..8].to_string(); - (format!("{}_{}", prefix, id), format!("{}_test@example.com", prefix)) -} +// Helper functions are now imported from test_setup module above // Test CLI with memory repository #[tokio::test] diff --git a/backend/crates/integration-tests/src/cross_interface_consistency_tests.rs b/backend/crates/integration-tests/src/cross_interface_consistency_tests.rs index 65f1dd9..0e37749 100644 --- a/backend/crates/integration-tests/src/cross_interface_consistency_tests.rs +++ b/backend/crates/integration-tests/src/cross_interface_consistency_tests.rs @@ -14,46 +14,13 @@ use domain::{CreateProduct, CreateUser, Product, UpdateProduct, UpdateUser, User use memory::{InMemoryUserRepository, InMemoryProductRepository}; use postgres::{PostgresUserRepository, PostgresProductRepository}; use sqlx::PgPool; -use sqlx::postgres::PgPoolOptions; -use std::env; use uuid::Uuid; use serial_test::serial; -// Helper functions for test setup -async fn setup_test_db() -> PgPool { - // Explicitly set DATABASE_URL to use sharenet_test database - std::env::set_var("DATABASE_URL", "postgres://postgres:password@localhost:5432/sharenet_test"); - - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to connect to test database"); - sqlx::migrate!("../../migrations") - .run(&pool) - .await - .expect("Failed to run migrations"); - cleanup_test_data(&pool).await; - pool -} +// Import the centralized test setup +use crate::test_setup::{setup_test_db, cleanup_test_data, unique_test_data}; -async fn cleanup_test_data(pool: &PgPool) { - let mut tx = pool.begin().await.expect("Failed to begin transaction"); - sqlx::query("TRUNCATE TABLE products, users RESTART IDENTITY CASCADE").execute(&mut *tx).await.expect("Failed to truncate tables"); - tx.commit().await.expect("Failed to commit cleanup transaction"); - // Assert tables are empty - let users_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users").fetch_one(pool).await.expect("Failed to count users"); - let products_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM products").fetch_one(pool).await.expect("Failed to count products"); - assert_eq!(users_count.0, 0, "Users table not empty after cleanup"); - assert_eq!(products_count.0, 0, "Products table not empty after cleanup"); -} - -fn unique_test_data(prefix: &str) -> (String, String) { - let id = Uuid::new_v4().to_string(); - (format!("{}_{}", prefix, id), format!("{}_{}@example.com", prefix, id)) -} +// Helper functions are now imported from test_setup module above // Test data structures for comparison #[derive(Debug, PartialEq, Clone)] diff --git a/backend/crates/integration-tests/src/cross_repository_consistency_tests.rs b/backend/crates/integration-tests/src/cross_repository_consistency_tests.rs index 4b186eb..dc66e15 100644 --- a/backend/crates/integration-tests/src/cross_repository_consistency_tests.rs +++ b/backend/crates/integration-tests/src/cross_repository_consistency_tests.rs @@ -13,47 +13,13 @@ use application::{Service, UseCase}; use domain::{CreateProduct, CreateUser, Product, UpdateProduct, UpdateUser, User}; use memory::{InMemoryUserRepository, InMemoryProductRepository}; use postgres::{PostgresUserRepository, PostgresProductRepository}; -use sqlx::PgPool; -use sqlx::postgres::PgPoolOptions; -use std::env; use uuid::Uuid; use serial_test::serial; -// Helper functions for test setup -async fn setup_test_db() -> PgPool { - // Explicitly set DATABASE_URL to use sharenet_test database - std::env::set_var("DATABASE_URL", "postgres://postgres:password@localhost:5432/sharenet_test"); - - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to connect to test database"); - sqlx::migrate!("../../migrations") - .run(&pool) - .await - .expect("Failed to run migrations"); - cleanup_test_data(&pool).await; - pool -} +// Import the centralized test setup +use crate::test_setup::{setup_test_db, unique_test_data}; -async fn cleanup_test_data(pool: &PgPool) { - let mut tx = pool.begin().await.expect("Failed to begin transaction"); - sqlx::query("TRUNCATE TABLE products, users RESTART IDENTITY CASCADE").execute(&mut *tx).await.expect("Failed to truncate tables"); - tx.commit().await.expect("Failed to commit cleanup transaction"); - // Assert tables are empty - let users_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users").fetch_one(pool).await.expect("Failed to count users"); - let products_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM products").fetch_one(pool).await.expect("Failed to count products"); - assert_eq!(users_count.0, 0, "Users table not empty after cleanup"); - assert_eq!(products_count.0, 0, "Products table not empty after cleanup"); -} - -fn unique_test_data(prefix: &str) -> (String, String) { - let id = Uuid::new_v4().to_string(); - (format!("{}_{}", prefix, id), format!("{}_{}@example.com", prefix, id)) -} +// Helper functions are now imported from test_setup module above // Test data structures for comparison #[derive(Debug, PartialEq, Clone)] diff --git a/backend/crates/integration-tests/src/lib.rs b/backend/crates/integration-tests/src/lib.rs index 360c2a6..d7c2693 100644 --- a/backend/crates/integration-tests/src/lib.rs +++ b/backend/crates/integration-tests/src/lib.rs @@ -9,6 +9,8 @@ * Copyright (c) 2024 Continuist */ +pub mod test_setup; + #[cfg(test)] pub mod api_postgres_tests; #[cfg(test)] diff --git a/backend/crates/integration-tests/src/migration_tests.rs b/backend/crates/integration-tests/src/migration_tests.rs index e33d1da..38e86f2 100644 --- a/backend/crates/integration-tests/src/migration_tests.rs +++ b/backend/crates/integration-tests/src/migration_tests.rs @@ -10,38 +10,15 @@ */ use sqlx::PgPool; -use sqlx::postgres::PgPoolOptions; use sqlx::Row; -use std::env; use serial_test::serial; + + /// Setup test database connection for migration tests async fn setup_migration_test_db() -> PgPool { - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); - - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to connect to test database"); - - // Clean up any existing data - cleanup_migration_test_data(&pool).await; - - pool -} - -/// Clean up test data for migration tests -async fn cleanup_migration_test_data(pool: &PgPool) { - let mut tx = pool.begin().await.expect("Failed to begin transaction"); - - // Drop tables if they exist (for migration tests) - sqlx::query("DROP TABLE IF EXISTS products CASCADE").execute(&mut *tx).await.expect("Failed to drop products table"); - sqlx::query("DROP TABLE IF EXISTS users CASCADE").execute(&mut *tx).await.expect("Failed to drop users table"); - sqlx::query("DROP TABLE IF EXISTS _sqlx_migrations CASCADE").execute(&mut *tx).await.expect("Failed to drop migrations table"); - - tx.commit().await.expect("Failed to commit cleanup transaction"); + // Use the centralized test setup that ensures migrations are run only once + crate::test_setup::setup_test_db().await } #[tokio::test] diff --git a/backend/crates/integration-tests/src/performance_tests.rs b/backend/crates/integration-tests/src/performance_tests.rs index 0146a84..e2453b2 100644 --- a/backend/crates/integration-tests/src/performance_tests.rs +++ b/backend/crates/integration-tests/src/performance_tests.rs @@ -20,8 +20,11 @@ use serde_json::json; use serial_test::serial; use tower::ServiceExt; +// Import the centralized test setup +use crate::test_setup::unique_test_data; + // Reuse AppState and helper functions from api_postgres_tests -use crate::api_postgres_tests::{create_test_app, unique_test_data}; +use crate::api_postgres_tests::create_test_app; /// Performance metrics structure #[derive(Debug)] diff --git a/backend/crates/integration-tests/src/test_setup.rs b/backend/crates/integration-tests/src/test_setup.rs new file mode 100644 index 0000000..e874b50 --- /dev/null +++ b/backend/crates/integration-tests/src/test_setup.rs @@ -0,0 +1,111 @@ +/* + * This file is part of Sharenet. + * + * Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. + * + * You may obtain a copy of the license at: + * https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * Copyright (c) 2024 Continuist + */ + +use sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; +use std::env; +use std::sync::Once; +use std::sync::Mutex; +use std::sync::Arc; +use std::sync::OnceLock; + +static INIT: Once = Once::new(); +static MIGRATION_LOCK: Mutex<()> = Mutex::new(()); + +/// Global test database pool that is initialized once for all tests +static TEST_POOL: OnceLock> = OnceLock::new(); + +/// Initialize the test database with migrations +/// This function ensures migrations are run only once across all tests +async fn initialize_test_database() -> Arc { + let database_url = env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); + + let pool = PgPoolOptions::new() + .max_connections(10) // Increased for concurrent tests + .connect(&database_url) + .await + .expect("Failed to connect to test database"); + + // Run migrations only once + let _lock = MIGRATION_LOCK.lock().unwrap(); + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + Arc::new(pool) +} + +/// Get a shared database pool for tests +/// This ensures all tests use the same database connection and migrations are run only once +pub async fn get_test_pool() -> Arc { + if let Some(pool) = TEST_POOL.get() { + pool.clone() + } else { + let pool = initialize_test_database().await; + let _ = TEST_POOL.set(pool.clone()); + pool + } +} + +/// Setup a test database connection with migrations already applied +/// This is the main function that test modules should use +pub async fn setup_test_db() -> PgPool { + let _pool_arc = get_test_pool().await; + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(&get_test_database_url()) + .await + .expect("Failed to connect to test database"); + + // Clean up any existing test data + cleanup_test_data(&pool).await; + + pool +} + +/// Clean up test data (does not drop tables, just clears data) +pub async fn cleanup_test_data(pool: &PgPool) { + let mut tx = pool.begin().await.expect("Failed to begin transaction"); + + // Delete in reverse order of foreign key dependencies + sqlx::query("DELETE FROM products").execute(&mut *tx).await.expect("Failed to delete products"); + sqlx::query("DELETE FROM users").execute(&mut *tx).await.expect("Failed to delete users"); + + tx.commit().await.expect("Failed to commit cleanup transaction"); +} + +/// Get the test database URL +pub fn get_test_database_url() -> String { + env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()) +} + +/// Generate unique test data to avoid conflicts between concurrent tests +pub fn unique_test_data(prefix: &str) -> (String, String) { + use uuid::Uuid; + let id = Uuid::new_v4().to_string()[..8].to_string(); + (format!("{}_{}", prefix, id), format!("{}_test@example.com", prefix)) +} + +/// Initialize the test environment +/// This should be called once at the start of the test suite +pub async fn initialize_test_environment() { + INIT.call_once(|| { + // This will be called only once, even if multiple tests call this function + println!("Initializing test environment..."); + }); + + // Ensure database is set up + let _pool = get_test_pool().await; + println!("Test environment initialized"); +} \ No newline at end of file diff --git a/backend/crates/integration-tests/src/tui_tests.rs b/backend/crates/integration-tests/src/tui_tests.rs index b506967..2cbdb08 100644 --- a/backend/crates/integration-tests/src/tui_tests.rs +++ b/backend/crates/integration-tests/src/tui_tests.rs @@ -14,9 +14,6 @@ use application::Service; use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser}; use memory::{InMemoryUserRepository, InMemoryProductRepository}; use postgres::{PostgresUserRepository, PostgresProductRepository}; -use sqlx::PgPool; -use sqlx::postgres::PgPoolOptions; -use std::env; use std::sync::Arc; use std::collections::HashMap; use tokio::sync::RwLock; @@ -25,34 +22,10 @@ use uuid::Uuid; use serial_test::serial; use application::UseCase; -// Helper functions for test setup -async fn setup_test_db() -> PgPool { - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string()); - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to connect to test database"); - sqlx::migrate!("../../migrations") - .run(&pool) - .await - .expect("Failed to run migrations"); - cleanup_test_data(&pool).await; - pool -} +// Import the centralized test setup +use crate::test_setup::{setup_test_db, unique_test_data}; -async fn cleanup_test_data(pool: &PgPool) { - let mut tx = pool.begin().await.expect("Failed to begin transaction"); - sqlx::query("DELETE FROM products").execute(&mut *tx).await.expect("Failed to delete products"); - sqlx::query("DELETE FROM users").execute(&mut *tx).await.expect("Failed to delete users"); - tx.commit().await.expect("Failed to commit cleanup transaction"); -} - -fn unique_test_data(prefix: &str) -> (String, String) { - let id = Uuid::new_v4().to_string()[..8].to_string(); - (format!("{}_{}", prefix, id), format!("{}_test@example.com", prefix)) -} +// Helper functions are now imported from test_setup module above // Mock services for testing TUI without real repositories #[derive(Clone)]