diff --git a/backend/crates/integration-tests/Cargo.toml b/backend/crates/integration-tests/Cargo.toml index 943f7e8..802d5ca 100644 --- a/backend/crates/integration-tests/Cargo.toml +++ b/backend/crates/integration-tests/Cargo.toml @@ -21,12 +21,15 @@ serial_test = "2.0" # Local crates api = { path = "../api" } application = { path = "../application" } +cli = { path = "../cli" } domain = { path = "../domain" } +memory = { path = "../memory" } postgres = { path = "../postgres" } # Additional test dependencies hyper = { version = "1.0", features = ["full"] } tower-http = { version = "0.5", features = ["cors", "trace"] } +clap = { version = "4.0", features = ["derive"] } [dev-dependencies] serial_test = "2.0" \ No newline at end of file diff --git a/backend/crates/integration-tests/src/cli_tests.rs b/backend/crates/integration-tests/src/cli_tests.rs new file mode 100644 index 0000000..2da37b6 --- /dev/null +++ b/backend/crates/integration-tests/src/cli_tests.rs @@ -0,0 +1,406 @@ +/* + * 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 anyhow::Result; +use clap::Parser; +use cli::{Cli, Commands, UserCommands, ProductCommands}; +use domain::{User, Product, CreateUser, UpdateUser, CreateProduct, UpdateProduct}; +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 +} + +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 CLI with memory repository +#[tokio::test] +#[serial] +async fn test_cli_with_memory_user_lifecycle() -> Result<()> { + let user_repo = InMemoryUserRepository::new(); + let product_repo = InMemoryProductRepository::new(); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Test user create + let (username, email) = unique_test_data("test_user"); + let cli = Cli::parse_from(&[ + "cli", "user", "create", "--username", &username, "--email", &email + ]); + + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user list + let cli = Cli::parse_from(&["cli", "user", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user get (we need to get the ID from the list first) + let users = user_service.list().await?; + assert!(!users.is_empty()); + let user_id = users[0].id; + + let cli = Cli::parse_from(&["cli", "user", "get", "--id", &user_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user update + let new_username = format!("{}_updated", username); + let cli = Cli::parse_from(&[ + "cli", "user", "update", "--id", &user_id.to_string(), "--username", &new_username + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user delete + let cli = Cli::parse_from(&["cli", "user", "delete", "--id", &user_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + Ok(()) +} + +#[tokio::test] +#[serial] +async fn test_cli_with_memory_product_lifecycle() -> Result<()> { + let user_repo = InMemoryUserRepository::new(); + let product_repo = InMemoryProductRepository::new(); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Test product create + let (name, description) = unique_test_data("test_product"); + let cli = Cli::parse_from(&[ + "cli", "product", "create", "--name", &name, "--description", &description + ]); + + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product list + let cli = Cli::parse_from(&["cli", "product", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product get (we need to get the ID from the list first) + let products = product_service.list().await?; + assert!(!products.is_empty()); + let product_id = products[0].id; + + let cli = Cli::parse_from(&["cli", "product", "get", "--id", &product_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product update + let new_name = format!("{}_updated", name); + let cli = Cli::parse_from(&[ + "cli", "product", "update", "--id", &product_id.to_string(), "--name", &new_name + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product delete + let cli = Cli::parse_from(&["cli", "product", "delete", "--id", &product_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + Ok(()) +} + +#[tokio::test] +#[serial] +async fn test_cli_with_memory_mixed_operations() -> Result<()> { + let user_repo = InMemoryUserRepository::new(); + let product_repo = InMemoryProductRepository::new(); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Create multiple users and products + for i in 1..=3 { + let (username, email) = unique_test_data(&format!("user_{}", i)); + let cli = Cli::parse_from(&[ + "cli", "user", "create", "--username", &username, "--email", &email + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + + let (name, description) = unique_test_data(&format!("product_{}", i)); + let cli = Cli::parse_from(&[ + "cli", "product", "create", "--name", &name, "--description", &description + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + } + + // List all users and products + let cli = Cli::parse_from(&["cli", "user", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + let cli = Cli::parse_from(&["cli", "product", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Verify counts + let users = user_service.list().await?; + let products = product_service.list().await?; + assert_eq!(users.len(), 3); + assert_eq!(products.len(), 3); + + Ok(()) +} + +// Test CLI with PostgreSQL repository +#[tokio::test] +#[serial] +async fn test_cli_with_postgres_user_lifecycle() -> Result<()> { + let pool = setup_test_db().await; + let user_repo = PostgresUserRepository::new(pool.clone()); + let product_repo = PostgresProductRepository::new(pool.clone()); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Test user create + let (username, email) = unique_test_data("test_user"); + let cli = Cli::parse_from(&[ + "cli", "user", "create", "--username", &username, "--email", &email + ]); + + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user list + let cli = Cli::parse_from(&["cli", "user", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user get (we need to get the ID from the list first) + let users = user_service.list().await?; + assert!(!users.is_empty()); + let user_id = users[0].id; + + let cli = Cli::parse_from(&["cli", "user", "get", "--id", &user_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user update + let new_username = format!("{}_updated", username); + let cli = Cli::parse_from(&[ + "cli", "user", "update", "--id", &user_id.to_string(), "--username", &new_username + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test user delete + let cli = Cli::parse_from(&["cli", "user", "delete", "--id", &user_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + Ok(()) +} + +#[tokio::test] +#[serial] +async fn test_cli_with_postgres_product_lifecycle() -> Result<()> { + let pool = setup_test_db().await; + let user_repo = PostgresUserRepository::new(pool.clone()); + let product_repo = PostgresProductRepository::new(pool.clone()); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Test product create + let (name, description) = unique_test_data("test_product"); + let cli = Cli::parse_from(&[ + "cli", "product", "create", "--name", &name, "--description", &description + ]); + + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product list + let cli = Cli::parse_from(&["cli", "product", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product get (we need to get the ID from the list first) + let products = product_service.list().await?; + assert!(!products.is_empty()); + let product_id = products[0].id; + + let cli = Cli::parse_from(&["cli", "product", "get", "--id", &product_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product update + let new_name = format!("{}_updated", name); + let cli = Cli::parse_from(&[ + "cli", "product", "update", "--id", &product_id.to_string(), "--name", &new_name + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Test product delete + let cli = Cli::parse_from(&["cli", "product", "delete", "--id", &product_id.to_string()]); + cli.run(user_service.clone(), product_service.clone()).await?; + + Ok(()) +} + +#[tokio::test] +#[serial] +async fn test_cli_with_postgres_mixed_operations() -> Result<()> { + let pool = setup_test_db().await; + let user_repo = PostgresUserRepository::new(pool.clone()); + let product_repo = PostgresProductRepository::new(pool.clone()); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Create multiple users and products + for i in 1..=3 { + let (username, email) = unique_test_data(&format!("user_{}", i)); + let cli = Cli::parse_from(&[ + "cli", "user", "create", "--username", &username, "--email", &email + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + + let (name, description) = unique_test_data(&format!("product_{}", i)); + let cli = Cli::parse_from(&[ + "cli", "product", "create", "--name", &name, "--description", &description + ]); + cli.run(user_service.clone(), product_service.clone()).await?; + } + + // List all users and products + let cli = Cli::parse_from(&["cli", "user", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + let cli = Cli::parse_from(&["cli", "product", "list"]); + cli.run(user_service.clone(), product_service.clone()).await?; + + // Verify counts + let users = user_service.list().await?; + let products = product_service.list().await?; + assert_eq!(users.len(), 3); + assert_eq!(products.len(), 3); + + Ok(()) +} + +// Test error handling +#[tokio::test] +#[serial] +async fn test_cli_error_handling() -> Result<()> { + let user_repo = InMemoryUserRepository::new(); + let product_repo = InMemoryProductRepository::new(); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Test getting non-existent user + let non_existent_id = Uuid::new_v4(); + let cli = Cli::parse_from(&["cli", "user", "get", "--id", &non_existent_id.to_string()]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + assert!(result.is_err()); + + // Test getting non-existent product + let cli = Cli::parse_from(&["cli", "product", "get", "--id", &non_existent_id.to_string()]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + assert!(result.is_err()); + + // Test updating non-existent user + let cli = Cli::parse_from(&[ + "cli", "user", "update", "--id", &non_existent_id.to_string(), "--username", "new_name" + ]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + assert!(result.is_err()); + + // Test deleting non-existent user + let cli = Cli::parse_from(&["cli", "user", "delete", "--id", &non_existent_id.to_string()]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + assert!(result.is_err()); + + Ok(()) +} + +// Test edge cases +#[tokio::test] +#[serial] +async fn test_cli_edge_cases() -> Result<()> { + let user_repo = InMemoryUserRepository::new(); + let product_repo = InMemoryProductRepository::new(); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + // Test empty strings (currently allowed by domain layer) + let cli = Cli::parse_from(&[ + "cli", "user", "create", "--username", "", "--email", "" + ]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + // Note: Empty strings are currently allowed by the domain layer + // This test documents the current behavior + + let cli = Cli::parse_from(&[ + "cli", "product", "create", "--name", "", "--description", "" + ]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + // Note: Empty strings are currently allowed by the domain layer + // This test documents the current behavior + + // Test very long strings + let long_string = "a".repeat(1000); + let cli = Cli::parse_from(&[ + "cli", "user", "create", "--username", &long_string, "--email", "test@example.com" + ]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + // Note: Very long strings are currently allowed by the domain layer + // This test documents the current behavior + + let cli = Cli::parse_from(&[ + "cli", "product", "create", "--name", &long_string, "--description", "test" + ]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + // Note: Very long strings are currently allowed by the domain layer + // This test documents the current behavior + + // Test special characters + let special_chars = "!@#$%^&*()_+-=[]{}|;':\",./<>?"; + let cli = Cli::parse_from(&[ + "cli", "user", "create", "--username", special_chars, "--email", "test@example.com" + ]); + let result = cli.run(user_service.clone(), product_service.clone()).await; + // Note: Special characters are currently allowed by the domain layer + // This test documents the current behavior + + Ok(()) +} + +// Test no command provided +#[tokio::test] +#[serial] +async fn test_cli_no_command() -> Result<()> { + let user_repo = InMemoryUserRepository::new(); + let product_repo = InMemoryProductRepository::new(); + let user_service = Service::new(user_repo); + let product_service = Service::new(product_repo); + + let cli = Cli::parse_from(&["cli"]); + cli.run(user_service, product_service).await?; + + Ok(()) +} \ No newline at end of file diff --git a/backend/crates/integration-tests/src/lib.rs b/backend/crates/integration-tests/src/lib.rs index 0e9545d..c5caf05 100644 --- a/backend/crates/integration-tests/src/lib.rs +++ b/backend/crates/integration-tests/src/lib.rs @@ -12,6 +12,8 @@ #[cfg(test)] pub mod api_postgres_tests; #[cfg(test)] +pub mod cli_tests; +#[cfg(test)] pub mod migration_tests; #[cfg(test)] pub mod performance_tests; \ No newline at end of file