Add CLI integration tests

This commit is contained in:
continuist 2025-06-24 22:36:17 -04:00
parent fbe383b76d
commit bb1bc4bf9b
3 changed files with 411 additions and 0 deletions

View file

@ -21,12 +21,15 @@ serial_test = "2.0"
# Local crates # Local crates
api = { path = "../api" } api = { path = "../api" }
application = { path = "../application" } application = { path = "../application" }
cli = { path = "../cli" }
domain = { path = "../domain" } domain = { path = "../domain" }
memory = { path = "../memory" }
postgres = { path = "../postgres" } postgres = { path = "../postgres" }
# Additional test dependencies # Additional test dependencies
hyper = { version = "1.0", features = ["full"] } hyper = { version = "1.0", features = ["full"] }
tower-http = { version = "0.5", features = ["cors", "trace"] } tower-http = { version = "0.5", features = ["cors", "trace"] }
clap = { version = "4.0", features = ["derive"] }
[dev-dependencies] [dev-dependencies]
serial_test = "2.0" serial_test = "2.0"

View file

@ -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 <continuist02@gmail.com>
*/
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(())
}

View file

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