Add migration tests to integration-tests crate

This commit is contained in:
continuist 2025-06-23 21:59:10 -04:00
parent 243f9f9aef
commit 34a1bd6119
2 changed files with 246 additions and 1 deletions

View file

@ -11,3 +11,5 @@
#[cfg(test)]
pub mod api_postgres_tests;
#[cfg(test)]
pub mod migration_tests;

View file

@ -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 <continuist02@gmail.com>
*/
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::<bool, _>(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::<bool, _>(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::<bool, _>(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<String> = users_columns.iter()
.map(|row| row.get::<String, _>(0))
.collect();
let product_column_names: Vec<String> = products_columns.iter()
.map(|row| row.get::<String, _>(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::<i64, _>(0);
let products_count = sqlx::query("SELECT COUNT(*) FROM products")
.fetch_one(&pool)
.await
.expect("Failed to count products")
.get::<i64, _>(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::<uuid::Uuid, _>(0), user_id, "User ID should match");
assert_eq!(product.get::<uuid::Uuid, _>(0), product_id, "Product ID should match");
}