Add cross-repository consistency integration tests

This commit is contained in:
continuist 2025-06-25 21:54:30 -04:00
parent ae5fb1cf31
commit 223d560ca0
2 changed files with 492 additions and 0 deletions

View file

@ -0,0 +1,490 @@
/*
* 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 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 {
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
}
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))
}
// Test data structures for comparison
#[derive(Debug, PartialEq, Clone)]
struct UserData {
username: String,
email: String,
}
impl From<&User> for UserData {
fn from(user: &User) -> Self {
Self {
username: user.username().to_string(),
email: user.email().to_string(),
}
}
}
#[derive(Debug, PartialEq, Clone)]
struct ProductData {
name: String,
description: String,
}
impl From<&Product> for ProductData {
fn from(product: &Product) -> Self {
Self {
name: product.name().to_string(),
description: product.description().to_string(),
}
}
}
// Helper function to compare user lists (ignoring IDs and timestamps)
fn compare_user_lists(memory_users: &[User], postgres_users: &[User]) {
let memory_data: Vec<UserData> = memory_users.iter().map(UserData::from).collect();
let postgres_data: Vec<UserData> = postgres_users.iter().map(UserData::from).collect();
assert_eq!(memory_data.len(), postgres_data.len(), "User count mismatch");
// Sort by username for consistent comparison
let mut memory_sorted = memory_data.clone();
let mut postgres_sorted = postgres_data.clone();
memory_sorted.sort_by(|a, b| a.username.cmp(&b.username));
postgres_sorted.sort_by(|a, b| a.username.cmp(&b.username));
assert_eq!(memory_sorted, postgres_sorted, "User data mismatch");
}
// Helper function to compare product lists (ignoring IDs and timestamps)
fn compare_product_lists(memory_products: &[Product], postgres_products: &[Product]) {
let memory_data: Vec<ProductData> = memory_products.iter().map(ProductData::from).collect();
let postgres_data: Vec<ProductData> = postgres_products.iter().map(ProductData::from).collect();
assert_eq!(memory_data.len(), postgres_data.len(), "Product count mismatch");
// Sort by name for consistent comparison
let mut memory_sorted = memory_data.clone();
let mut postgres_sorted = postgres_data.clone();
memory_sorted.sort_by(|a, b| a.name.cmp(&b.name));
postgres_sorted.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(memory_sorted, postgres_sorted, "Product data mismatch");
}
#[tokio::test]
#[serial]
async fn test_user_crud_consistency() {
let pool = setup_test_db().await;
// Create services
let memory_user_repo = InMemoryUserRepository::new();
let postgres_user_repo = PostgresUserRepository::new(pool.clone());
let memory_user_service = Service::new(memory_user_repo);
let postgres_user_service = Service::new(postgres_user_repo);
// Test data
let (username1, email1) = unique_test_data("consistency_user1");
let (username2, email2) = unique_test_data("consistency_user2");
// Test 1: Create users in both repositories
let create_user1 = CreateUser::new(username1.clone(), email1.clone()).unwrap();
let create_user2 = CreateUser::new(username2.clone(), email2.clone()).unwrap();
let memory_user1 = memory_user_service.create(create_user1.clone()).await.unwrap();
let memory_user2 = memory_user_service.create(create_user2.clone()).await.unwrap();
let postgres_user1 = postgres_user_service.create(create_user1).await.unwrap();
let postgres_user2 = postgres_user_service.create(create_user2).await.unwrap();
// Verify created users have same data (ignoring IDs and timestamps)
assert_eq!(memory_user1.username(), postgres_user1.username());
assert_eq!(memory_user1.email(), postgres_user1.email());
assert_eq!(memory_user2.username(), postgres_user2.username());
assert_eq!(memory_user2.email(), postgres_user2.email());
// Test 2: List users and compare
let memory_users = memory_user_service.list().await.unwrap();
let postgres_users = postgres_user_service.list().await.unwrap();
compare_user_lists(&memory_users, &postgres_users);
// Test 3: Get individual users
let memory_retrieved1 = memory_user_service.get(memory_user1.id()).await.unwrap();
let postgres_retrieved1 = postgres_user_service.get(postgres_user1.id()).await.unwrap();
assert_eq!(memory_retrieved1.username(), postgres_retrieved1.username());
assert_eq!(memory_retrieved1.email(), postgres_retrieved1.email());
// Test 4: Update users
let update_data = UpdateUser::new(Some("updated_username".to_string()), Some("updated@example.com".to_string())).unwrap();
let memory_updated = memory_user_service.update(memory_user1.id(), update_data.clone()).await.unwrap();
let postgres_updated = postgres_user_service.update(postgres_user1.id(), update_data).await.unwrap();
assert_eq!(memory_updated.username(), postgres_updated.username());
assert_eq!(memory_updated.email(), postgres_updated.email());
// Test 5: Delete users
memory_user_service.delete(memory_user1.id()).await.unwrap();
postgres_user_service.delete(postgres_user1.id()).await.unwrap();
// Verify deletion
let memory_users_after_delete = memory_user_service.list().await.unwrap();
let postgres_users_after_delete = postgres_user_service.list().await.unwrap();
assert_eq!(memory_users_after_delete.len(), 1);
assert_eq!(postgres_users_after_delete.len(), 1);
compare_user_lists(&memory_users_after_delete, &postgres_users_after_delete);
}
#[tokio::test]
#[serial]
async fn test_product_crud_consistency() {
let pool = setup_test_db().await;
// Create services
let memory_product_repo = InMemoryProductRepository::new();
let postgres_product_repo = PostgresProductRepository::new(pool.clone());
let memory_product_service = Service::new(memory_product_repo);
let postgres_product_service = Service::new(postgres_product_repo);
// Test data
let (name1, _) = unique_test_data("consistency_product1");
let (name2, _) = unique_test_data("consistency_product2");
let description1 = "Test product description 1";
let description2 = "Test product description 2";
// Test 1: Create products in both repositories
let create_product1 = CreateProduct::new(name1.clone(), description1.to_string()).unwrap();
let create_product2 = CreateProduct::new(name2.clone(), description2.to_string()).unwrap();
let memory_product1 = memory_product_service.create(create_product1.clone()).await.unwrap();
let memory_product2 = memory_product_service.create(create_product2.clone()).await.unwrap();
let postgres_product1 = postgres_product_service.create(create_product1).await.unwrap();
let postgres_product2 = postgres_product_service.create(create_product2).await.unwrap();
// Verify created products have same data (ignoring IDs and timestamps)
assert_eq!(memory_product1.name(), postgres_product1.name());
assert_eq!(memory_product1.description(), postgres_product1.description());
assert_eq!(memory_product2.name(), postgres_product2.name());
assert_eq!(memory_product2.description(), postgres_product2.description());
// Test 2: List products and compare
let memory_products = memory_product_service.list().await.unwrap();
let postgres_products = postgres_product_service.list().await.unwrap();
compare_product_lists(&memory_products, &postgres_products);
// Test 3: Get individual products
let memory_retrieved1 = memory_product_service.get(memory_product1.id()).await.unwrap();
let postgres_retrieved1 = postgres_product_service.get(postgres_product1.id()).await.unwrap();
assert_eq!(memory_retrieved1.name(), postgres_retrieved1.name());
assert_eq!(memory_retrieved1.description(), postgres_retrieved1.description());
// Test 4: Update products
let update_data = UpdateProduct::new(Some("updated_product_name".to_string()), Some("updated description".to_string())).unwrap();
let memory_updated = memory_product_service.update(memory_product1.id(), update_data.clone()).await.unwrap();
let postgres_updated = postgres_product_service.update(postgres_product1.id(), update_data).await.unwrap();
assert_eq!(memory_updated.name(), postgres_updated.name());
assert_eq!(memory_updated.description(), postgres_updated.description());
// Test 5: Delete products
memory_product_service.delete(memory_product1.id()).await.unwrap();
postgres_product_service.delete(postgres_product1.id()).await.unwrap();
// Verify deletion
let memory_products_after_delete = memory_product_service.list().await.unwrap();
let postgres_products_after_delete = postgres_product_service.list().await.unwrap();
assert_eq!(memory_products_after_delete.len(), 1);
assert_eq!(postgres_products_after_delete.len(), 1);
compare_product_lists(&memory_products_after_delete, &postgres_products_after_delete);
}
#[tokio::test]
#[serial]
async fn test_error_handling_consistency() {
let pool = setup_test_db().await;
// Create services
let memory_user_repo = InMemoryUserRepository::new();
let postgres_user_repo = PostgresUserRepository::new(pool.clone());
let memory_user_service = Service::new(memory_user_repo);
let postgres_user_service = Service::new(postgres_user_repo);
let memory_product_repo = InMemoryProductRepository::new();
let postgres_product_repo = PostgresProductRepository::new(pool.clone());
let memory_product_service = Service::new(memory_product_repo);
let postgres_product_service = Service::new(postgres_product_repo);
// Test 1: Get non-existent user
let non_existent_id = Uuid::new_v4();
let memory_user_result = memory_user_service.get(non_existent_id).await;
let postgres_user_result = postgres_user_service.get(non_existent_id).await;
assert!(memory_user_result.is_err());
assert!(postgres_user_result.is_err());
// Both should return NotFound error
assert!(matches!(memory_user_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
assert!(matches!(postgres_user_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
// Test 2: Get non-existent product
let memory_product_result = memory_product_service.get(non_existent_id).await;
let postgres_product_result = postgres_product_service.get(non_existent_id).await;
assert!(memory_product_result.is_err());
assert!(postgres_product_result.is_err());
assert!(matches!(memory_product_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
assert!(matches!(postgres_product_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
// Test 3: Update non-existent user
let update_data = UpdateUser::new(Some("new_username".to_string()), None).unwrap();
let memory_update_result = memory_user_service.update(non_existent_id, update_data.clone()).await;
let postgres_update_result = postgres_user_service.update(non_existent_id, update_data).await;
assert!(memory_update_result.is_err());
assert!(postgres_update_result.is_err());
assert!(matches!(memory_update_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
assert!(matches!(postgres_update_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
// Test 4: Delete non-existent user
let memory_delete_result = memory_user_service.delete(non_existent_id).await;
let postgres_delete_result = postgres_user_service.delete(non_existent_id).await;
assert!(memory_delete_result.is_err());
assert!(postgres_delete_result.is_err());
assert!(matches!(memory_delete_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
assert!(matches!(postgres_delete_result.unwrap_err(), application::ApplicationError::Domain(domain::DomainError::NotFound(_))));
}
#[tokio::test]
#[serial]
async fn test_validation_consistency() {
let pool = setup_test_db().await;
// Create services
let memory_user_repo = InMemoryUserRepository::new();
let postgres_user_repo = PostgresUserRepository::new(pool.clone());
let _memory_user_service = Service::new(memory_user_repo);
let _postgres_user_service = Service::new(postgres_user_repo);
let memory_product_repo = InMemoryProductRepository::new();
let postgres_product_repo = PostgresProductRepository::new(pool.clone());
let _memory_product_service = Service::new(memory_product_repo);
let _postgres_product_service = Service::new(postgres_product_repo);
// Test 1: Create user with empty username
let create_user_empty_username = CreateUser::new("".to_string(), "test@example.com".to_string());
assert!(create_user_empty_username.is_err());
assert!(matches!(create_user_empty_username.unwrap_err(), domain::DomainError::InvalidInput(_)));
// Test 2: Create product with empty name
let create_product_empty_name = CreateProduct::new("".to_string(), "description".to_string());
assert!(create_product_empty_name.is_err());
assert!(matches!(create_product_empty_name.unwrap_err(), domain::DomainError::InvalidInput(_)));
// Test 3: Update user with empty username
let update_user_empty_username = UpdateUser::new(Some("".to_string()), None);
assert!(update_user_empty_username.is_err());
assert!(matches!(update_user_empty_username.unwrap_err(), domain::DomainError::InvalidInput(_)));
// Test 4: Update product with empty name
let update_product_empty_name = UpdateProduct::new(Some("".to_string()), None);
assert!(update_product_empty_name.is_err());
assert!(matches!(update_product_empty_name.unwrap_err(), domain::DomainError::InvalidInput(_)));
}
#[tokio::test]
#[serial]
async fn test_concurrent_operations_consistency() {
let pool = setup_test_db().await;
// Create services
let memory_user_repo = InMemoryUserRepository::new();
let postgres_user_repo = PostgresUserRepository::new(pool.clone());
let memory_user_service = Service::new(memory_user_repo);
let postgres_user_service = Service::new(postgres_user_repo);
let memory_product_repo = InMemoryProductRepository::new();
let postgres_product_repo = PostgresProductRepository::new(pool.clone());
let memory_product_service = Service::new(memory_product_repo);
let postgres_product_service = Service::new(postgres_product_repo);
// Test concurrent creation of multiple users and products
let mut memory_user_handles = Vec::new();
let mut postgres_user_handles = Vec::new();
let mut memory_product_handles = Vec::new();
let mut postgres_product_handles = Vec::new();
// Spawn concurrent operations
for i in 0..5 {
let (username, email) = unique_test_data(&format!("concurrent_user_{}", i));
let create_user = CreateUser::new(username, email).unwrap();
let memory_service = memory_user_service.clone();
let postgres_service = postgres_user_service.clone();
let create_user_for_memory = create_user.clone();
let create_user_for_postgres = create_user;
memory_user_handles.push(tokio::spawn(async move {
memory_service.create(create_user_for_memory).await
}));
postgres_user_handles.push(tokio::spawn(async move {
postgres_service.create(create_user_for_postgres).await
}));
let (name, _) = unique_test_data(&format!("concurrent_product_{}", i));
let create_product = CreateProduct::new(name, format!("Description {}", i)).unwrap();
let memory_service = memory_product_service.clone();
let postgres_service = postgres_product_service.clone();
let create_product_for_memory = create_product.clone();
let create_product_for_postgres = create_product;
memory_product_handles.push(tokio::spawn(async move {
memory_service.create(create_product_for_memory).await
}));
postgres_product_handles.push(tokio::spawn(async move {
postgres_service.create(create_product_for_postgres).await
}));
}
// Wait for all operations to complete
for handle in memory_user_handles {
handle.await.unwrap().unwrap();
}
for handle in postgres_user_handles {
handle.await.unwrap().unwrap();
}
for handle in memory_product_handles {
handle.await.unwrap().unwrap();
}
for handle in postgres_product_handles {
handle.await.unwrap().unwrap();
}
// Verify both repositories have the same data
let memory_users = memory_user_service.list().await.unwrap();
let postgres_users = postgres_user_service.list().await.unwrap();
let memory_products = memory_product_service.list().await.unwrap();
let postgres_products = postgres_product_service.list().await.unwrap();
assert_eq!(memory_users.len(), 5);
assert_eq!(postgres_users.len(), 5);
assert_eq!(memory_products.len(), 5);
assert_eq!(postgres_products.len(), 5);
compare_user_lists(&memory_users, &postgres_users);
compare_product_lists(&memory_products, &postgres_products);
}
#[tokio::test]
#[serial]
async fn test_data_persistence_consistency() {
let pool = setup_test_db().await;
// Create services
let memory_user_repo = InMemoryUserRepository::new();
let postgres_user_repo = PostgresUserRepository::new(pool.clone());
let memory_user_service = Service::new(memory_user_repo);
let postgres_user_service = Service::new(postgres_user_repo);
let memory_product_repo = InMemoryProductRepository::new();
let postgres_product_repo = PostgresProductRepository::new(pool.clone());
let memory_product_service = Service::new(memory_product_repo);
let postgres_product_service = Service::new(postgres_product_repo);
// Create test data
let (username, email) = unique_test_data("persistence_user");
let (name, _) = unique_test_data("persistence_product");
let create_user = CreateUser::new(username.clone(), email.clone()).unwrap();
let create_product = CreateProduct::new(name.clone(), "Test description".to_string()).unwrap();
let memory_user = memory_user_service.create(create_user.clone()).await.unwrap();
let postgres_user = postgres_user_service.create(create_user).await.unwrap();
let memory_product = memory_product_service.create(create_product.clone()).await.unwrap();
let postgres_product = postgres_product_service.create(create_product).await.unwrap();
// Verify data was created
assert_eq!(memory_user.username(), username);
assert_eq!(postgres_user.username(), username);
assert_eq!(memory_product.name(), name);
assert_eq!(postgres_product.name(), name);
// Test that data persists across service instances (for PostgreSQL)
// Memory repository data is lost when service is dropped, but PostgreSQL persists
let new_postgres_user_repo = PostgresUserRepository::new(pool.clone());
let new_postgres_product_repo = PostgresProductRepository::new(pool.clone());
let new_postgres_user_service = Service::new(new_postgres_user_repo);
let new_postgres_product_service = Service::new(new_postgres_product_repo);
let retrieved_user = new_postgres_user_service.get(postgres_user.id()).await.unwrap();
let retrieved_product = new_postgres_product_service.get(postgres_product.id()).await.unwrap();
assert_eq!(retrieved_user.username(), username);
assert_eq!(retrieved_user.email(), email);
assert_eq!(retrieved_product.name(), name);
assert_eq!(retrieved_product.description(), "Test description");
// Memory repository should still have the data in the same service instance
let memory_retrieved_user = memory_user_service.get(memory_user.id()).await.unwrap();
let memory_retrieved_product = memory_product_service.get(memory_product.id()).await.unwrap();
assert_eq!(memory_retrieved_user.username(), username);
assert_eq!(memory_retrieved_user.email(), email);
assert_eq!(memory_retrieved_product.name(), name);
assert_eq!(memory_retrieved_product.description(), "Test description");
}

View file

@ -14,6 +14,8 @@ pub mod api_postgres_tests;
#[cfg(test)]
pub mod cli_tests;
#[cfg(test)]
pub mod cross_repository_consistency_tests;
#[cfg(test)]
pub mod migration_tests;
#[cfg(test)]
pub mod performance_tests;