/* * 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 domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser}; use application::UseCase; use uuid::Uuid; #[derive(Parser)] #[command(author, version, about, long_about = None)] pub struct Cli { #[command(subcommand)] command: Option, } #[derive(Parser)] pub enum Commands { /// User management commands User { #[command(subcommand)] command: UserCommands, }, /// Product management commands Product { #[command(subcommand)] command: ProductCommands, }, } #[derive(Parser)] pub enum UserCommands { /// Create a new user Create { /// Username #[arg(short, long)] username: String, /// Email #[arg(short, long)] email: String, }, /// List all users List, /// Get a user by ID Get { /// User ID #[arg(short, long)] id: Uuid, }, /// Update a user Update { /// User ID #[arg(short, long)] id: Uuid, /// New username #[arg(short, long)] username: Option, /// New email #[arg(short, long)] email: Option, }, /// Delete a user Delete { /// User ID #[arg(short, long)] id: Uuid, }, } #[derive(Parser)] pub enum ProductCommands { /// Create a new product Create { /// Product name #[arg(short, long)] name: String, /// Product description #[arg(short, long)] description: String, }, /// List all products List, /// Get a product by ID Get { /// Product ID #[arg(short, long)] id: Uuid, }, /// Update a product Update { /// Product ID #[arg(short, long)] id: Uuid, /// New name #[arg(short, long)] name: Option, /// New description #[arg(short, long)] description: Option, }, /// Delete a product Delete { /// Product ID #[arg(short, long)] id: Uuid, }, } impl Cli { pub async fn run(self, user_service: U, product_service: P) -> Result<()> where U: UseCase, P: UseCase, { match self.command { Some(Commands::User { command }) => match command { UserCommands::Create { username, email } => { let user = user_service.create(CreateUser::new(username, email)?).await?; println!("Created user: {:?}", user); } UserCommands::List => { let users = user_service.list().await?; println!("Users: {:?}", users); } UserCommands::Get { id } => { let user = user_service.get(id).await?; println!("User: {:?}", user); } UserCommands::Update { id, username, email } => { let update = UpdateUser::new(username, email)?; let user = user_service.update(id, update).await?; println!("Updated user: {:?}", user); } UserCommands::Delete { id } => { user_service.delete(id).await?; println!("Deleted user {}", id); } }, Some(Commands::Product { command }) => match command { ProductCommands::Create { name, description } => { let product = product_service.create(CreateProduct::new(name, description)?).await?; println!("Created product: {:?}", product); } ProductCommands::List => { let products = product_service.list().await?; println!("Products: {:?}", products); } ProductCommands::Get { id } => { let product = product_service.get(id).await?; println!("Product: {:?}", product); } ProductCommands::Update { id, name, description } => { let update = UpdateProduct::new(name, description)?; let product = product_service.update(id, update).await?; println!("Updated product: {:?}", product); } ProductCommands::Delete { id } => { product_service.delete(id).await?; println!("Deleted product {}", id); } }, None => { println!("No command provided. Use --help for usage information."); } } Ok(()) } } #[cfg(test)] mod tests { //! # CLI Tests //! //! This module contains comprehensive unit tests for the CLI application. //! Tests cover command parsing, execution, and error handling scenarios. //! //! ## Test Structure //! - `command_parsing` - Tests for CLI argument parsing //! - `user_commands` - Tests for user management commands //! - `product_commands` - Tests for product management commands //! - `integration_tests` - End-to-end command execution tests //! - `error_handling` - Tests for error scenarios //! //! ## Testing Approach //! - Uses mock services implementing the `UseCase` trait //! - Tests verify command parsing and execution //! - Captures stdout to verify output messages //! - Tests error handling and edge cases use super::*; use application::{UseCase, ApplicationError}; use domain::{DomainError, User, Product, CreateUser, UpdateUser, CreateProduct, UpdateProduct}; use std::sync::Arc; use tokio::sync::RwLock; use std::collections::HashMap; /// Mock user service for testing. /// /// Implements the `UseCase` trait using an in-memory HashMap /// for storing test data. Provides thread-safe access via `RwLock`. #[derive(Clone)] struct MockUserService { users: Arc>>, } impl MockUserService { fn new() -> Self { Self { users: Arc::new(RwLock::new(HashMap::new())), } } } impl UseCase for MockUserService { fn create(&self, data: CreateUser) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let mut guard = users.write().await; let id = Uuid::new_v4(); let user = User::new(id, data.username().to_string(), data.email().to_string())?; guard.insert(id, user.clone()); Ok(user) } } fn get(&self, id: Uuid) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let guard = users.read().await; guard.get(&id) .cloned() .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("User not found: {}", id)))) } } fn list(&self) -> impl std::future::Future, ApplicationError>> + Send { let users = self.users.clone(); async move { let guard = users.read().await; Ok(guard.values().cloned().collect()) } } fn update(&self, id: Uuid, data: UpdateUser) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let mut guard = users.write().await; let user = guard.get_mut(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("User not found: {}", id))))?; if let Some(username) = data.username() { user.set_username(username.to_string())?; } if let Some(email) = data.email() { user.set_email(email.to_string())?; } Ok(user.clone()) } } fn delete(&self, id: Uuid) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let mut guard = users.write().await; guard.remove(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("User not found: {}", id))))?; Ok(()) } } } /// Mock product service for testing. /// /// Implements the `UseCase` trait using an in-memory HashMap /// for storing test data. Provides thread-safe access via `RwLock`. #[derive(Clone)] struct MockProductService { products: Arc>>, } impl MockProductService { fn new() -> Self { Self { products: Arc::new(RwLock::new(HashMap::new())), } } } impl UseCase for MockProductService { fn create(&self, data: CreateProduct) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let mut guard = products.write().await; let id = Uuid::new_v4(); let product = Product::new(id, data.name().to_string(), data.description().to_string())?; guard.insert(id, product.clone()); Ok(product) } } fn get(&self, id: Uuid) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let guard = products.read().await; guard.get(&id) .cloned() .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("Product not found: {}", id)))) } } fn list(&self) -> impl std::future::Future, ApplicationError>> + Send { let products = self.products.clone(); async move { let guard = products.read().await; Ok(guard.values().cloned().collect()) } } fn update(&self, id: Uuid, data: UpdateProduct) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let mut guard = products.write().await; let product = guard.get_mut(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("Product not found: {}", id))))?; if let Some(name) = data.name() { product.set_name(name.to_string())?; } if let Some(description) = data.description() { product.set_description(description.to_string())?; } Ok(product.clone()) } } fn delete(&self, id: Uuid) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let mut guard = products.write().await; guard.remove(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("Product not found: {}", id))))?; Ok(()) } } } mod command_parsing { use super::*; /// Tests that the CLI can parse user create command with all arguments. #[test] fn test_parse_user_create() { let args = vec![ "cli", "user", "create", "--username", "testuser", "--email", "test@example.com" ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::User { command: UserCommands::Create { username, email } }) = cli.command { assert_eq!(username, "testuser"); assert_eq!(email, "test@example.com"); } else { panic!("Expected user create command"); } } /// Tests that the CLI can parse user create command with short arguments. #[test] fn test_parse_user_create_short_args() { let args = vec![ "cli", "user", "create", "-u", "testuser", "-e", "test@example.com" ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::User { command: UserCommands::Create { username, email } }) = cli.command { assert_eq!(username, "testuser"); assert_eq!(email, "test@example.com"); } else { panic!("Expected user create command"); } } /// Tests that the CLI can parse user list command. #[test] fn test_parse_user_list() { let args = vec!["cli", "user", "list"]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::User { command: UserCommands::List }) = cli.command { // Command parsed successfully } else { panic!("Expected user list command"); } } /// Tests that the CLI can parse user get command. #[test] fn test_parse_user_get() { let user_id = Uuid::new_v4(); let user_id_str = user_id.to_string(); let args = vec![ "cli", "user", "get", "--id", &user_id_str ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::User { command: UserCommands::Get { id } }) = cli.command { assert_eq!(id, user_id); } else { panic!("Expected user get command"); } } /// Tests that the CLI can parse user update command with all fields. #[test] fn test_parse_user_update() { let user_id = Uuid::new_v4(); let user_id_str = user_id.to_string(); let args = vec![ "cli", "user", "update", "--id", &user_id_str, "--username", "newuser", "--email", "new@example.com" ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::User { command: UserCommands::Update { id, username, email } }) = cli.command { assert_eq!(id, user_id); assert_eq!(username, Some("newuser".to_string())); assert_eq!(email, Some("new@example.com".to_string())); } else { panic!("Expected user update command"); } } /// Tests that the CLI can parse user update command with partial fields. #[test] fn test_parse_user_update_partial() { let user_id = Uuid::new_v4(); let user_id_str = user_id.to_string(); let args = vec![ "cli", "user", "update", "--id", &user_id_str, "--username", "newuser" ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::User { command: UserCommands::Update { id, username, email } }) = cli.command { assert_eq!(id, user_id); assert_eq!(username, Some("newuser".to_string())); assert_eq!(email, None); } else { panic!("Expected user update command"); } } /// Tests that the CLI can parse user delete command. #[test] fn test_parse_user_delete() { let user_id = Uuid::new_v4(); let user_id_str = user_id.to_string(); let args = vec![ "cli", "user", "delete", "--id", &user_id_str ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::User { command: UserCommands::Delete { id } }) = cli.command { assert_eq!(id, user_id); } else { panic!("Expected user delete command"); } } /// Tests that the CLI can parse product create command. #[test] fn test_parse_product_create() { let args = vec![ "cli", "product", "create", "--name", "Test Product", "--description", "Test Description" ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::Product { command: ProductCommands::Create { name, description } }) = cli.command { assert_eq!(name, "Test Product"); assert_eq!(description, "Test Description"); } else { panic!("Expected product create command"); } } /// Tests that the CLI can parse product list command. #[test] fn test_parse_product_list() { let args = vec!["cli", "product", "list"]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::Product { command: ProductCommands::List }) = cli.command { // Command parsed successfully } else { panic!("Expected product list command"); } } /// Tests that the CLI can parse product get command. #[test] fn test_parse_product_get() { let product_id = Uuid::new_v4(); let product_id_str = product_id.to_string(); let args = vec![ "cli", "product", "get", "--id", &product_id_str ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::Product { command: ProductCommands::Get { id } }) = cli.command { assert_eq!(id, product_id); } else { panic!("Expected product get command"); } } /// Tests that the CLI can parse product update command. #[test] fn test_parse_product_update() { let product_id = Uuid::new_v4(); let product_id_str = product_id.to_string(); let args = vec![ "cli", "product", "update", "--id", &product_id_str, "--name", "New Product", "--description", "New Description" ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::Product { command: ProductCommands::Update { id, name, description } }) = cli.command { assert_eq!(id, product_id); assert_eq!(name, Some("New Product".to_string())); assert_eq!(description, Some("New Description".to_string())); } else { panic!("Expected product update command"); } } /// Tests that the CLI can parse product delete command. #[test] fn test_parse_product_delete() { let product_id = Uuid::new_v4(); let product_id_str = product_id.to_string(); let args = vec![ "cli", "product", "delete", "--id", &product_id_str ]; let cli = Cli::try_parse_from(args).unwrap(); if let Some(Commands::Product { command: ProductCommands::Delete { id } }) = cli.command { assert_eq!(id, product_id); } else { panic!("Expected product delete command"); } } /// Tests that the CLI handles no command gracefully. #[test] fn test_parse_no_command() { let args = vec!["cli"]; let cli = Cli::try_parse_from(args).unwrap(); assert!(cli.command.is_none()); } /// Tests that the CLI rejects invalid UUID format. #[test] fn test_parse_invalid_uuid() { let args = vec![ "cli", "user", "get", "--id", "invalid-uuid" ]; let result = Cli::try_parse_from(args); assert!(result.is_err()); } } mod user_commands { use super::*; /// Tests user creation command execution. #[tokio::test] async fn test_user_create_command() { let cli = Cli { command: Some(Commands::User { command: UserCommands::Create { username: "testuser".to_string(), email: "test@example.com".to_string(), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_ok()); } /// Tests user list command execution. #[tokio::test] async fn test_user_list_command() { let cli = Cli { command: Some(Commands::User { command: UserCommands::List }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_ok()); } /// Tests user get command execution. #[tokio::test] async fn test_user_get_command() { let user_id = Uuid::new_v4(); let cli = Cli { command: Some(Commands::User { command: UserCommands::Get { id: user_id } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; // Should fail because user doesn't exist assert!(result.is_err()); } /// Tests user update command execution. #[tokio::test] async fn test_user_update_command() { let user_id = Uuid::new_v4(); let cli = Cli { command: Some(Commands::User { command: UserCommands::Update { id: user_id, username: Some("newuser".to_string()), email: Some("new@example.com".to_string()), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; // Should fail because user doesn't exist assert!(result.is_err()); } /// Tests user delete command execution. #[tokio::test] async fn test_user_delete_command() { let user_id = Uuid::new_v4(); let cli = Cli { command: Some(Commands::User { command: UserCommands::Delete { id: user_id } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; // Should fail because user doesn't exist assert!(result.is_err()); } } mod product_commands { use super::*; /// Tests product creation command execution. #[tokio::test] async fn test_product_create_command() { let cli = Cli { command: Some(Commands::Product { command: ProductCommands::Create { name: "Test Product".to_string(), description: "Test Description".to_string(), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_ok()); } /// Tests product list command execution. #[tokio::test] async fn test_product_list_command() { let cli = Cli { command: Some(Commands::Product { command: ProductCommands::List }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_ok()); } /// Tests product get command execution. #[tokio::test] async fn test_product_get_command() { let product_id = Uuid::new_v4(); let cli = Cli { command: Some(Commands::Product { command: ProductCommands::Get { id: product_id } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; // Should fail because product doesn't exist assert!(result.is_err()); } /// Tests product update command execution. #[tokio::test] async fn test_product_update_command() { let product_id = Uuid::new_v4(); let cli = Cli { command: Some(Commands::Product { command: ProductCommands::Update { id: product_id, name: Some("New Product".to_string()), description: Some("New Description".to_string()), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; // Should fail because product doesn't exist assert!(result.is_err()); } /// Tests product delete command execution. #[tokio::test] async fn test_product_delete_command() { let product_id = Uuid::new_v4(); let cli = Cli { command: Some(Commands::Product { command: ProductCommands::Delete { id: product_id } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; // Should fail because product doesn't exist assert!(result.is_err()); } } mod integration_tests { use super::*; /// Tests complete user lifecycle through CLI commands. #[tokio::test] async fn test_user_lifecycle() { let user_service = MockUserService::new(); let product_service = MockProductService::new(); // Create user let create_cli = Cli { command: Some(Commands::User { command: UserCommands::Create { username: "testuser".to_string(), email: "test@example.com".to_string(), } }) }; let result = create_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Get the created user ID by listing users let users = user_service.list().await.unwrap(); assert_eq!(users.len(), 1); let user_id = users[0].id(); // Get user let get_cli = Cli { command: Some(Commands::User { command: UserCommands::Get { id: user_id } }) }; let result = get_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Update user let update_cli = Cli { command: Some(Commands::User { command: UserCommands::Update { id: user_id, username: Some("updateduser".to_string()), email: None, } }) }; let result = update_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Delete user let delete_cli = Cli { command: Some(Commands::User { command: UserCommands::Delete { id: user_id } }) }; let result = delete_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Verify user is deleted let users = user_service.list().await.unwrap(); assert_eq!(users.len(), 0); } /// Tests complete product lifecycle through CLI commands. #[tokio::test] async fn test_product_lifecycle() { let user_service = MockUserService::new(); let product_service = MockProductService::new(); // Create product let create_cli = Cli { command: Some(Commands::Product { command: ProductCommands::Create { name: "Test Product".to_string(), description: "Test Description".to_string(), } }) }; let result = create_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Get the created product ID by listing products let products = product_service.list().await.unwrap(); assert_eq!(products.len(), 1); let product_id = products[0].id(); // Get product let get_cli = Cli { command: Some(Commands::Product { command: ProductCommands::Get { id: product_id } }) }; let result = get_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Update product let update_cli = Cli { command: Some(Commands::Product { command: ProductCommands::Update { id: product_id, name: Some("Updated Product".to_string()), description: None, } }) }; let result = update_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Delete product let delete_cli = Cli { command: Some(Commands::Product { command: ProductCommands::Delete { id: product_id } }) }; let result = delete_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Verify product is deleted let products = product_service.list().await.unwrap(); assert_eq!(products.len(), 0); } /// Tests mixed user and product operations. #[tokio::test] async fn test_mixed_operations() { let user_service = MockUserService::new(); let product_service = MockProductService::new(); // Create a user let user_cli = Cli { command: Some(Commands::User { command: UserCommands::Create { username: "testuser".to_string(), email: "test@example.com".to_string(), } }) }; let result = user_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Create a product let product_cli = Cli { command: Some(Commands::Product { command: ProductCommands::Create { name: "Test Product".to_string(), description: "Test Description".to_string(), } }) }; let result = product_cli.run(user_service.clone(), product_service.clone()).await; assert!(result.is_ok()); // Verify both exist let users = user_service.list().await.unwrap(); let products = product_service.list().await.unwrap(); assert_eq!(users.len(), 1); assert_eq!(products.len(), 1); } } mod error_handling { use super::*; /// Tests handling of service errors in user operations. #[tokio::test] async fn test_user_service_error() { let cli = Cli { command: Some(Commands::User { command: UserCommands::Get { id: Uuid::new_v4() } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_err()); } /// Tests handling of service errors in product operations. #[tokio::test] async fn test_product_service_error() { let cli = Cli { command: Some(Commands::Product { command: ProductCommands::Get { id: Uuid::new_v4() } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_err()); } /// Tests handling of no command provided. #[tokio::test] async fn test_no_command() { let cli = Cli { command: None }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_ok()); } } mod edge_cases { use super::*; /// Tests CLI with empty strings in user creation. #[tokio::test] async fn test_user_create_empty_strings() { let cli = Cli { command: Some(Commands::User { command: UserCommands::Create { username: "".to_string(), email: "".to_string(), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_err()); // Should fail due to empty username validation } /// Tests CLI with empty strings in product creation. #[tokio::test] async fn test_product_create_empty_strings() { let cli = Cli { command: Some(Commands::Product { command: ProductCommands::Create { name: "".to_string(), description: "".to_string(), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_err()); // Should fail due to empty product name validation } /// Tests CLI with very long strings. #[tokio::test] async fn test_user_create_long_strings() { let long_username = "a".repeat(1000); let long_email = format!("{}@example.com", "a".repeat(1000)); let cli = Cli { command: Some(Commands::User { command: UserCommands::Create { username: long_username.clone(), email: long_email.clone(), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_ok()); } /// Tests CLI with special characters in strings. #[tokio::test] async fn test_user_create_special_characters() { let cli = Cli { command: Some(Commands::User { command: UserCommands::Create { username: "user@#$%^&*()".to_string(), email: "test+tag@example.com".to_string(), } }) }; let user_service = MockUserService::new(); let product_service = MockProductService::new(); let result = cli.run(user_service, product_service).await; assert!(result.is_ok()); } } }