From ae5fb1cf3100ecc53c8f39f566565a6a7ae50663 Mon Sep 17 00:00:00 2001 From: continuist Date: Wed, 25 Jun 2025 21:42:03 -0400 Subject: [PATCH] Add missing TUI integration tests --- .../crates/integration-tests/src/tui_tests.rs | 968 +++++++++++++++++- backend/crates/tui/src/lib.rs | 38 + 2 files changed, 996 insertions(+), 10 deletions(-) diff --git a/backend/crates/integration-tests/src/tui_tests.rs b/backend/crates/integration-tests/src/tui_tests.rs index e50fbe8..b506967 100644 --- a/backend/crates/integration-tests/src/tui_tests.rs +++ b/backend/crates/integration-tests/src/tui_tests.rs @@ -9,17 +9,965 @@ * Copyright (c) 2024 Continuist */ -#[cfg(test)] -mod tests { - use tui::App; +use anyhow::Result; +use application::Service; +use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser}; +use memory::{InMemoryUserRepository, InMemoryProductRepository}; +use postgres::{PostgresUserRepository, PostgresProductRepository}; +use sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; +use std::env; +use std::sync::Arc; +use std::collections::HashMap; +use tokio::sync::RwLock; +use tui::App; +use uuid::Uuid; +use serial_test::serial; +use application::UseCase; - #[test] - fn test_app_initialization() { - let app = App::new(); - assert_eq!(app.input(), ""); - assert_eq!(app.messages(), &vec!["Welcome to Sharenet CLI!".to_string()]); - assert!(!app.should_quit()); +// 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)) +} + +// Mock services for testing TUI without real repositories +#[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()) + .map_err(|e| application::ApplicationError::Domain(e))?; + guard.insert(id, user.clone()); + Ok(user) + } } - // More integration tests will be added here + 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(|| application::ApplicationError::Domain(domain::DomainError::NotFound(format!("User not found: {}", id)))) + } + } + + fn list(&self) -> impl std::future::Future, application::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(|| application::ApplicationError::Domain(domain::DomainError::NotFound(format!("User not found: {}", id))))?; + + if let Some(username) = data.username() { + user.set_username(username.to_string()) + .map_err(|e| application::ApplicationError::Domain(e))?; + } + if let Some(email) = data.email() { + user.set_email(email.to_string()) + .map_err(|e| application::ApplicationError::Domain(e))?; + } + 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(|| application::ApplicationError::Domain(domain::DomainError::NotFound(format!("User not found: {}", id))))?; + Ok(()) + } + } +} + +#[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()) + .map_err(|e| application::ApplicationError::Domain(e))?; + 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(|| application::ApplicationError::Domain(domain::DomainError::NotFound(format!("Product not found: {}", id)))) + } + } + + fn list(&self) -> impl std::future::Future, application::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(|| application::ApplicationError::Domain(domain::DomainError::NotFound(format!("Product not found: {}", id))))?; + + if let Some(name) = data.name() { + product.set_name(name.to_string()) + .map_err(|e| application::ApplicationError::Domain(e))?; + } + if let Some(description) = data.description() { + product.set_description(description.to_string()) + .map_err(|e| application::ApplicationError::Domain(e))?; + } + 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(|| application::ApplicationError::Domain(domain::DomainError::NotFound(format!("Product not found: {}", id))))?; + Ok(()) + } + } +} + +// Helper function to simulate TUI command execution +async fn execute_tui_command( + app: &mut App, + command: &str, + user_service: &U, + product_service: &P, +) where + U: UseCase, + P: UseCase, +{ + // Simulate the command processing logic from the TUI + app.add_message(format!("> {}", command)); + + match command.trim() { + "exit" => { + app.add_message("Goodbye!".to_string()); + // Note: In the real TUI, this would set should_quit to true + // For testing purposes, we just add the message + } + "help" => { + print_help(app); + } + cmd if cmd.starts_with("user create") => { + match parse_user_create(cmd) { + Ok((username, email)) => { + match CreateUser::new(username, email) { + Ok(create_user) => { + match user_service.create(create_user).await { + Ok(user) => app.add_message(format!("Created user: {:?}", user)), + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + "user list" => { + match user_service.list().await { + Ok(users) => app.add_message(format!("Users: {:?}", users)), + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + cmd if cmd.starts_with("product create") => { + match parse_product_create(cmd) { + Ok((name, description)) => { + match CreateProduct::new(name, description) { + Ok(create_product) => { + match product_service.create(create_product).await { + Ok(product) => app.add_message(format!("Created product: {:?}", product)), + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + "product list" => { + match product_service.list().await { + Ok(products) => app.add_message(format!("Products: {:?}", products)), + Err(e) => app.add_message(format!("Error: {}", e)), + } + } + "" => {} + _ => { + app.add_message("Unknown command. Type 'help' for available commands.".to_string()); + } + } +} + +fn print_help(app: &mut App) { + app.add_message("\nAvailable commands:".to_string()); + app.add_message(" user create -u -e ".to_string()); + app.add_message(" Example: user create -u \"john doe\" -e \"john@example.com\"".to_string()); + app.add_message(" user list".to_string()); + app.add_message(" product create -n -d ".to_string()); + app.add_message(" Example: product create -n \"My Product\" -d \"A great product description\"".to_string()); + app.add_message(" product list".to_string()); + app.add_message("\nTips:".to_string()); + app.add_message(" - Use quotes for values with spaces".to_string()); + app.add_message(" - Use Up/Down arrows to navigate command history".to_string()); + app.add_message(" - Press Esc to exit".to_string()); + app.add_message(" - Type 'help' to show this message".to_string()); +} + +fn parse_user_create(cmd: &str) -> anyhow::Result<(String, String)> { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + if parts.len() < 6 { + return Err(anyhow::anyhow!( + "Invalid command format. Use: user create -u -e \nExample: user create -u \"john doe\" -e \"john@example.com\"" + )); + } + + let mut username = None; + let mut email = None; + let mut current_arg = None; + let mut current_value = Vec::new(); + + // Skip "user create" command + let mut i = 2; + while i < parts.len() { + match parts[i] { + "-u" => { + if let Some(arg_type) = current_arg { + match arg_type { + "username" => username = Some(current_value.join(" ")), + "email" => email = Some(current_value.join(" ")), + _ => {} + } + } + current_arg = Some("username"); + current_value.clear(); + i += 1; + } + "-e" => { + if let Some(arg_type) = current_arg { + match arg_type { + "username" => username = Some(current_value.join(" ")), + "email" => email = Some(current_value.join(" ")), + _ => {} + } + } + current_arg = Some("email"); + current_value.clear(); + i += 1; + } + _ => { + if current_arg.is_some() { + current_value.push(parts[i].trim_matches('"')); + } + i += 1; + } + } + } + + // Handle the last argument + if let Some(arg_type) = current_arg { + match arg_type { + "username" => username = Some(current_value.join(" ")), + "email" => email = Some(current_value.join(" ")), + _ => {} + } + } + + match (username, email) { + (Some(u), Some(e)) if !u.is_empty() && !e.is_empty() => Ok((u, e)), + _ => Err(anyhow::anyhow!( + "Invalid command format. Use: user create -u -e \nExample: user create -u \"john doe\" -e \"john@example.com\"" + )), + } +} + +fn parse_product_create(cmd: &str) -> anyhow::Result<(String, String)> { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + if parts.len() < 6 { + return Err(anyhow::anyhow!( + "Invalid command format. Use: product create -n -d \nExample: product create -n \"My Product\" -d \"A great product description\"" + )); + } + + let mut name = None; + let mut description = None; + let mut current_arg = None; + let mut current_value = Vec::new(); + + // Skip "product create" command + let mut i = 2; + while i < parts.len() { + match parts[i] { + "-n" => { + if let Some(arg_type) = current_arg { + match arg_type { + "name" => name = Some(current_value.join(" ")), + "description" => description = Some(current_value.join(" ")), + _ => {} + } + } + current_arg = Some("name"); + current_value.clear(); + i += 1; + } + "-d" => { + if let Some(arg_type) = current_arg { + match arg_type { + "name" => name = Some(current_value.join(" ")), + "description" => description = Some(current_value.join(" ")), + _ => {} + } + } + current_arg = Some("description"); + current_value.clear(); + i += 1; + } + _ => { + if current_arg.is_some() { + current_value.push(parts[i].trim_matches('"')); + } + i += 1; + } + } + } + + // Handle the last argument + if let Some(arg_type) = current_arg { + match arg_type { + "name" => name = Some(current_value.join(" ")), + "description" => description = Some(current_value.join(" ")), + _ => {} + } + } + + match (name, description) { + (Some(n), Some(d)) if !n.is_empty() && !d.is_empty() => Ok((n, d)), + _ => Err(anyhow::anyhow!( + "Invalid command format. Use: product create -n -d \nExample: product create -n \"My Product\" -d \"A great product description\"" + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod app_initialization { + use super::*; + + #[test] + fn test_app_initialization() { + let app = App::new(); + assert_eq!(app.input(), ""); + assert_eq!(app.messages(), &vec!["Welcome to Sharenet CLI!".to_string()]); + assert!(!app.should_quit()); + } + + #[test] + fn test_app_message_handling() { + let mut app = App::new(); + app.add_message("Test message".to_string()); + assert!(app.messages().contains(&"Test message".to_string())); + } + + #[test] + fn test_app_input_handling() { + let mut app = App::new(); + app.insert_char('a'); + app.insert_char('b'); + app.insert_char('c'); + assert_eq!(app.input(), "abc"); + } + + #[test] + fn test_app_cursor_movement() { + let mut app = App::new(); + // Add some input first + app.insert_char('a'); + app.insert_char('b'); + app.insert_char('c'); + // Test cursor movement + app.move_cursor(-1); + app.move_cursor(-1); + app.move_cursor(-1); + // We can't directly test cursor position, but we can test that it doesn't panic + } + + #[test] + fn test_app_history() { + let mut app = App::new(); + app.add_to_history("command1".to_string()); + app.add_to_history("command2".to_string()); + app.add_to_history("command3".to_string()); + + // We can't directly test history navigation without private field access + // but we can test that adding to history doesn't panic + } + } + + mod command_parsing { + use super::*; + + #[test] + fn test_parse_user_create_valid() { + let cmd = "user create -u alice -e alice@example.com"; + let result = parse_user_create(cmd); + assert!(result.is_ok()); + let (username, email) = result.unwrap(); + assert_eq!(username, "alice"); + assert_eq!(email, "alice@example.com"); + } + + #[test] + fn test_parse_user_create_with_spaces() { + let cmd = "user create -u \"john doe\" -e \"john@example.com\""; + let result = parse_user_create(cmd); + assert!(result.is_ok()); + let (username, email) = result.unwrap(); + assert_eq!(username, "john doe"); + assert_eq!(email, "john@example.com"); + } + + #[test] + fn test_parse_user_create_invalid_format() { + let cmd = "user create -u alice"; + let result = parse_user_create(cmd); + assert!(result.is_err()); + } + + #[test] + fn test_parse_user_create_empty_values() { + let cmd = "user create -u \"\" -e \"\""; + let result = parse_user_create(cmd); + assert!(result.is_err()); + } + + #[test] + fn test_parse_product_create_valid() { + let cmd = "product create -n widget -d description"; + let result = parse_product_create(cmd); + assert!(result.is_ok()); + let (name, description) = result.unwrap(); + assert_eq!(name, "widget"); + assert_eq!(description, "description"); + } + + #[test] + fn test_parse_product_create_with_spaces() { + let cmd = "product create -n \"My Product\" -d \"A great product description\""; + let result = parse_product_create(cmd); + assert!(result.is_ok()); + let (name, description) = result.unwrap(); + assert_eq!(name, "My Product"); + assert_eq!(description, "A great product description"); + } + + #[test] + fn test_parse_product_create_invalid_format() { + let cmd = "product create -n widget"; + let result = parse_product_create(cmd); + assert!(result.is_err()); + } + + #[test] + fn test_parse_product_create_empty_values() { + let cmd = "product create -n \"\" -d \"\""; + let result = parse_product_create(cmd); + assert!(result.is_err()); + } + } + + mod tui_integration_with_mock_services { + use super::*; + + #[tokio::test] + async fn test_tui_user_create_command() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + let (username, email) = unique_test_data("tui_user"); + let command = format!("user create -u {} -e {}", username, email); + + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + + // Check that the command was displayed + assert!(app.messages().iter().any(|msg| msg.contains(&format!("> {}", command)))); + + // Check that a user was created successfully + let users = user_service.list().await.unwrap(); + assert_eq!(users.len(), 1); + assert_eq!(users[0].username(), username); + assert_eq!(users[0].email(), email); + + // Check that success message was displayed + assert!(app.messages().iter().any(|msg| msg.contains("Created user:"))); + } + + #[tokio::test] + async fn test_tui_user_list_command() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + // Create a user first + let create_user = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); + user_service.create(create_user).await.unwrap(); + + execute_tui_command(&mut app, "user list", &user_service, &product_service).await; + + // Check that the command was displayed + assert!(app.messages().iter().any(|msg| msg.contains("> user list"))); + + // Check that users were listed + assert!(app.messages().iter().any(|msg| msg.contains("Users:"))); + assert!(app.messages().iter().any(|msg| msg.contains("testuser"))); + } + + #[tokio::test] + async fn test_tui_product_create_command() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + let (name, _) = unique_test_data("tui_product"); + let description = "Test product description"; + let command = format!("product create -n {} -d {}", name, description); + + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + + // Check that the command was displayed + assert!(app.messages().iter().any(|msg| msg.contains(&format!("> {}", command)))); + + // Check that a product was created successfully + let products = product_service.list().await.unwrap(); + assert_eq!(products.len(), 1); + assert_eq!(products[0].name(), name); + assert_eq!(products[0].description(), description); + + // Check that success message was displayed + assert!(app.messages().iter().any(|msg| msg.contains("Created product:"))); + } + + #[tokio::test] + async fn test_tui_product_list_command() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + // Create a product first + let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); + product_service.create(create_product).await.unwrap(); + + execute_tui_command(&mut app, "product list", &user_service, &product_service).await; + + // Check that the command was displayed + assert!(app.messages().iter().any(|msg| msg.contains("> product list"))); + + // Check that products were listed + assert!(app.messages().iter().any(|msg| msg.contains("Products:"))); + assert!(app.messages().iter().any(|msg| msg.contains("Test Product"))); + } + + #[tokio::test] + async fn test_tui_help_command() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + execute_tui_command(&mut app, "help", &user_service, &product_service).await; + + // Check that help was displayed + assert!(app.messages().iter().any(|msg| msg.contains("Available commands:"))); + assert!(app.messages().iter().any(|msg| msg.contains("user create"))); + assert!(app.messages().iter().any(|msg| msg.contains("product create"))); + } + + #[tokio::test] + async fn test_tui_exit_command() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + execute_tui_command(&mut app, "exit", &user_service, &product_service).await; + + // Check that exit message was displayed + assert!(app.messages().iter().any(|msg| msg.contains("Goodbye!"))); + // Note: We can't test should_quit() in integration tests since it's private + } + + #[tokio::test] + async fn test_tui_unknown_command() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + execute_tui_command(&mut app, "unknown command", &user_service, &product_service).await; + + // Check that error message was displayed + assert!(app.messages().iter().any(|msg| msg.contains("Unknown command"))); + } + + #[tokio::test] + async fn test_tui_invalid_user_create() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + execute_tui_command(&mut app, "user create -u", &user_service, &product_service).await; + + // Check that error message was displayed + assert!(app.messages().iter().any(|msg| msg.contains("Error:"))); + } + + #[tokio::test] + async fn test_tui_invalid_product_create() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + execute_tui_command(&mut app, "product create -n", &user_service, &product_service).await; + + // Check that error message was displayed + assert!(app.messages().iter().any(|msg| msg.contains("Error:"))); + } + } + + mod tui_integration_with_memory_services { + use super::*; + + #[tokio::test] + #[serial] + async fn test_tui_with_memory_user_lifecycle() { + 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 mut app = App::new(); + + // Create user via TUI command + let (username, email) = unique_test_data("memory_user"); + let command = format!("user create -u {} -e {}", username, email); + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + + // List users via TUI command + execute_tui_command(&mut app, "user list", &user_service, &product_service).await; + + // Verify user was created + let users = user_service.list().await.unwrap(); + assert_eq!(users.len(), 1); + assert_eq!(users[0].username(), username); + assert_eq!(users[0].email(), email); + } + + #[tokio::test] + #[serial] + async fn test_tui_with_memory_product_lifecycle() { + 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 mut app = App::new(); + + // Create product via TUI command + let (name, _) = unique_test_data("memory_product"); + let description = "Test product description"; + let command = format!("product create -n {} -d {}", name, description); + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + + // List products via TUI command + execute_tui_command(&mut app, "product list", &user_service, &product_service).await; + + // Verify product was created + let products = product_service.list().await.unwrap(); + assert_eq!(products.len(), 1); + assert_eq!(products[0].name(), name); + assert_eq!(products[0].description(), description); + } + + #[tokio::test] + #[serial] + async fn test_tui_with_memory_mixed_operations() { + 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 mut app = App::new(); + + // Create multiple users and products via TUI commands + for i in 1..=3 { + let (username, email) = unique_test_data(&format!("user_{}", i)); + let command = format!("user create -u {} -e {}", username, email); + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + + let (name, _) = unique_test_data(&format!("product_{}", i)); + let command = format!("product create -n {} -d \"Description {}\"", name, i); + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + } + + // List all users and products + execute_tui_command(&mut app, "user list", &user_service, &product_service).await; + execute_tui_command(&mut app, "product list", &user_service, &product_service).await; + + // Verify counts + let users = user_service.list().await.unwrap(); + let products = product_service.list().await.unwrap(); + assert_eq!(users.len(), 3); + assert_eq!(products.len(), 3); + } + } + + mod tui_integration_with_postgres_services { + use super::*; + + #[tokio::test] + #[serial] + async fn test_tui_with_postgres_user_lifecycle() { + 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); + let mut app = App::new(); + + // Create user via TUI command + let (username, email) = unique_test_data("postgres_user"); + let command = format!("user create -u {} -e {}", username, email); + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + + // List users via TUI command + execute_tui_command(&mut app, "user list", &user_service, &product_service).await; + + // Verify user was created + let users = user_service.list().await.unwrap(); + assert_eq!(users.len(), 1); + assert_eq!(users[0].username(), username); + assert_eq!(users[0].email(), email); + } + + #[tokio::test] + #[serial] + async fn test_tui_with_postgres_product_lifecycle() { + 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); + let mut app = App::new(); + + // Create product via TUI command + let (name, _) = unique_test_data("postgres_product"); + let description = "Test product description"; + let command = format!("product create -n {} -d {}", name, description); + execute_tui_command(&mut app, &command, &user_service, &product_service).await; + + // List products via TUI command + execute_tui_command(&mut app, "product list", &user_service, &product_service).await; + + // Verify product was created + let products = product_service.list().await.unwrap(); + assert_eq!(products.len(), 1); + assert_eq!(products[0].name(), name); + assert_eq!(products[0].description(), description); + } + + #[tokio::test] + #[serial] + async fn test_tui_with_postgres_error_handling() { + 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); + let mut app = App::new(); + + // Test invalid user creation + execute_tui_command(&mut app, "user create -u \"\" -e \"\"", &user_service, &product_service).await; + + // Test invalid product creation + execute_tui_command(&mut app, "product create -n \"\" -d \"\"", &user_service, &product_service).await; + + // Verify no data was created + let users = user_service.list().await.unwrap(); + let products = product_service.list().await.unwrap(); + assert_eq!(users.len(), 0); + assert_eq!(products.len(), 0); + } + } + + mod tui_error_handling { + use super::*; + + #[tokio::test] + async fn test_tui_command_parsing_errors() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + // Test various invalid command formats + let invalid_commands = vec![ + "user create", + "user create -u", + "user create -u test", + "user create -e test@example.com", + "product create", + "product create -n", + "product create -n test", + "product create -d test", + ]; + + for command in invalid_commands { + execute_tui_command(&mut app, command, &user_service, &product_service).await; + assert!(app.messages().iter().any(|msg| msg.contains("Error:"))); + } + } + + #[tokio::test] + async fn test_tui_empty_commands() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + // Test empty command + execute_tui_command(&mut app, "", &user_service, &product_service).await; + + // Should not add any error messages for empty commands + let error_messages: Vec<&String> = app.messages().iter().filter(|msg| msg.contains("Error:")).collect(); + assert_eq!(error_messages.len(), 0); + } + + #[tokio::test] + async fn test_tui_whitespace_commands() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + // Test whitespace-only command + execute_tui_command(&mut app, " ", &user_service, &product_service).await; + + // Should not add any error messages for whitespace-only commands + let error_messages: Vec<&String> = app.messages().iter().filter(|msg| msg.contains("Error:")).collect(); + assert_eq!(error_messages.len(), 0); + } + } + + mod tui_command_history { + use super::*; + + #[tokio::test] + async fn test_tui_command_history_functionality() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + // Execute several commands + let commands = vec![ + "help", + "user list", + "product list", + "user create -u test -e test@example.com", + ]; + + for command in commands { + execute_tui_command(&mut app, command, &user_service, &product_service).await; + } + + // We can't directly test command history without private field access + // but we can test that commands are processed without errors + assert!(app.messages().len() > 4); // Should have at least the command messages + } + + #[tokio::test] + async fn test_tui_empty_commands_not_added_to_history() { + let mut app = App::new(); + let user_service = MockUserService::new(); + let product_service = MockProductService::new(); + + // Execute empty and whitespace commands + execute_tui_command(&mut app, "", &user_service, &product_service).await; + execute_tui_command(&mut app, " ", &user_service, &product_service).await; + execute_tui_command(&mut app, "help", &user_service, &product_service).await; + + // We can't directly test command history without private field access + // but we can test that commands are processed without errors + assert!(app.messages().len() > 1); // Should have at least the help message + } + } } \ No newline at end of file diff --git a/backend/crates/tui/src/lib.rs b/backend/crates/tui/src/lib.rs index e6e2ff4..e78dc21 100644 --- a/backend/crates/tui/src/lib.rs +++ b/backend/crates/tui/src/lib.rs @@ -102,6 +102,44 @@ impl App { pub fn should_quit(&self) -> bool { self.should_quit } + + // Test helper methods + #[cfg(test)] + pub fn set_should_quit(&mut self, should_quit: bool) { + self.should_quit = should_quit; + } + + #[cfg(test)] + pub fn cursor_position(&self) -> usize { + self.cursor_position + } + + #[cfg(test)] + pub fn set_input(&mut self, input: String) { + let len = input.len(); + self.input = input; + self.cursor_position = len; + } + + #[cfg(test)] + pub fn set_cursor_position(&mut self, position: usize) { + self.cursor_position = position.min(self.input.len()); + } + + #[cfg(test)] + pub fn history_index(&self) -> Option { + self.history_index + } + + #[cfg(test)] + pub fn set_history_index(&mut self, index: Option) { + self.history_index = index; + } + + #[cfg(test)] + pub fn command_history(&self) -> &VecDeque { + &self.command_history + } } pub async fn run_tui(user_service: U, product_service: P) -> anyhow::Result<()>