diff --git a/backend/crates/integration-tests/src/lib.rs b/backend/crates/integration-tests/src/lib.rs index 615c193..65262cc 100644 --- a/backend/crates/integration-tests/src/lib.rs +++ b/backend/crates/integration-tests/src/lib.rs @@ -10,4 +10,6 @@ */ #[cfg(test)] -pub mod api_postgres_tests; \ No newline at end of file +pub mod api_postgres_tests; +#[cfg(test)] +pub mod migration_tests; \ No newline at end of file diff --git a/backend/crates/integration-tests/src/migration_tests.rs b/backend/crates/integration-tests/src/migration_tests.rs new file mode 100644 index 0000000..e33d1da --- /dev/null +++ b/backend/crates/integration-tests/src/migration_tests.rs @@ -0,0 +1,243 @@ +/* + * 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 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"); +} + +#[tokio::test] +#[serial] +async fn test_migrations_can_be_applied() { + let pool = setup_migration_test_db().await; + + // Run migrations + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + // Verify tables exist + let users_table_exists = sqlx::query( + "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'users')" + ) + .fetch_one(&pool) + .await + .expect("Failed to check users table") + .get::(0); + + let products_table_exists = sqlx::query( + "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'products')" + ) + .fetch_one(&pool) + .await + .expect("Failed to check products table") + .get::(0); + + let migrations_table_exists = sqlx::query( + "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = '_sqlx_migrations')" + ) + .fetch_one(&pool) + .await + .expect("Failed to check migrations table") + .get::(0); + + assert!(users_table_exists, "Users table should exist after migrations"); + assert!(products_table_exists, "Products table should exist after migrations"); + assert!(migrations_table_exists, "Migrations table should exist after migrations"); + + // Verify table structure + let users_columns = sqlx::query( + "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'users' ORDER BY ordinal_position" + ) + .fetch_all(&pool) + .await + .expect("Failed to get users table columns"); + + let products_columns = sqlx::query( + "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'products' ORDER BY ordinal_position" + ) + .fetch_all(&pool) + .await + .expect("Failed to get products table columns"); + + // Check that expected columns exist + let user_column_names: Vec = users_columns.iter() + .map(|row| row.get::(0)) + .collect(); + + let product_column_names: Vec = products_columns.iter() + .map(|row| row.get::(0)) + .collect(); + + assert!(user_column_names.contains(&"id".to_string()), "Users table should have id column"); + assert!(user_column_names.contains(&"username".to_string()), "Users table should have username column"); + assert!(user_column_names.contains(&"email".to_string()), "Users table should have email column"); + assert!(user_column_names.contains(&"created_at".to_string()), "Users table should have created_at column"); + assert!(user_column_names.contains(&"updated_at".to_string()), "Users table should have updated_at column"); + + assert!(product_column_names.contains(&"id".to_string()), "Products table should have id column"); + assert!(product_column_names.contains(&"name".to_string()), "Products table should have name column"); + assert!(product_column_names.contains(&"description".to_string()), "Products table should have description column"); + assert!(product_column_names.contains(&"created_at".to_string()), "Products table should have created_at column"); + assert!(product_column_names.contains(&"updated_at".to_string()), "Products table should have updated_at column"); +} + +#[tokio::test] +#[serial] +async fn test_migrations_are_idempotent() { + let pool = setup_migration_test_db().await; + + // Run migrations first time + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations first time"); + + // Run migrations second time (should be idempotent) + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations second time"); + + // Verify tables still exist and are functional + let users_count = sqlx::query("SELECT COUNT(*) FROM users") + .fetch_one(&pool) + .await + .expect("Failed to count users") + .get::(0); + + let products_count = sqlx::query("SELECT COUNT(*) FROM products") + .fetch_one(&pool) + .await + .expect("Failed to count products") + .get::(0); + + assert_eq!(users_count, 0, "Users table should be empty after migrations"); + assert_eq!(products_count, 0, "Products table should be empty after migrations"); +} + +#[tokio::test] +#[serial] +async fn test_migration_constraints() { + let pool = setup_migration_test_db().await; + + // Run migrations + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + // Test unique constraint on username + sqlx::query("INSERT INTO users (id, username, email, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)") + .bind(uuid::Uuid::new_v4()) + .bind("testuser") + .bind("test@example.com") + .bind(chrono::Utc::now()) + .bind(chrono::Utc::now()) + .execute(&pool) + .await + .expect("Failed to insert first user"); + + // Try to insert another user with the same username (should fail) + let result = sqlx::query("INSERT INTO users (id, username, email, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)") + .bind(uuid::Uuid::new_v4()) + .bind("testuser") + .bind("test2@example.com") + .bind(chrono::Utc::now()) + .bind(chrono::Utc::now()) + .execute(&pool) + .await; + + assert!(result.is_err(), "Should not be able to insert user with duplicate username"); +} + +#[tokio::test] +#[serial] +async fn test_migration_data_types() { + let pool = setup_migration_test_db().await; + + // Run migrations + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + // Test that UUID columns accept valid UUIDs + let user_id = uuid::Uuid::new_v4(); + let product_id = uuid::Uuid::new_v4(); + + sqlx::query("INSERT INTO users (id, username, email, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)") + .bind(user_id) + .bind("testuser") + .bind("test@example.com") + .bind(chrono::Utc::now()) + .bind(chrono::Utc::now()) + .execute(&pool) + .await + .expect("Failed to insert user with UUID"); + + sqlx::query("INSERT INTO products (id, name, description, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)") + .bind(product_id) + .bind("Test Product") + .bind("Test Description") + .bind(chrono::Utc::now()) + .bind(chrono::Utc::now()) + .execute(&pool) + .await + .expect("Failed to insert product with UUID"); + + // Verify the data was inserted correctly + let user = sqlx::query("SELECT id FROM users WHERE id = $1") + .bind(user_id) + .fetch_one(&pool) + .await + .expect("Failed to fetch user"); + + let product = sqlx::query("SELECT id FROM products WHERE id = $1") + .bind(product_id) + .fetch_one(&pool) + .await + .expect("Failed to fetch product"); + + assert_eq!(user.get::(0), user_id, "User ID should match"); + assert_eq!(product.get::(0), product_id, "Product ID should match"); +} \ No newline at end of file