Add missing TUI integration tests

This commit is contained in:
continuist 2025-06-25 21:42:03 -04:00
parent 8ef852beac
commit ae5fb1cf31
2 changed files with 996 additions and 10 deletions

View file

@ -9,9 +9,428 @@
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
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;
// 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<RwLock<HashMap<Uuid, User>>>,
}
impl MockUserService {
fn new() -> Self {
Self {
users: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl UseCase<User> for MockUserService {
fn create(&self, data: CreateUser) -> impl std::future::Future<Output = Result<User, application::ApplicationError>> + 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)
}
}
fn get(&self, id: Uuid) -> impl std::future::Future<Output = Result<User, application::ApplicationError>> + 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<Output = Result<Vec<User>, 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<Output = Result<User, application::ApplicationError>> + 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<Output = Result<(), application::ApplicationError>> + 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<RwLock<HashMap<Uuid, Product>>>,
}
impl MockProductService {
fn new() -> Self {
Self {
products: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl UseCase<Product> for MockProductService {
fn create(&self, data: CreateProduct) -> impl std::future::Future<Output = Result<Product, application::ApplicationError>> + 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<Output = Result<Product, application::ApplicationError>> + 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<Output = Result<Vec<Product>, 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<Output = Result<Product, application::ApplicationError>> + 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<Output = Result<(), application::ApplicationError>> + 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<U, P>(
app: &mut App,
command: &str,
user_service: &U,
product_service: &P,
) where
U: UseCase<User>,
P: UseCase<Product>,
{
// 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 <username> -e <email>".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 <name> -d <description>".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 <username> -e <email>\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 <username> -e <email>\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 <name> -d <description>\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 <name> -d <description>\nExample: product create -n \"My Product\" -d \"A great product description\""
)),
}
}
#[cfg(test)]
mod tests {
use tui::App;
use super::*;
mod app_initialization {
use super::*;
#[test]
fn test_app_initialization() {
@ -21,5 +440,534 @@ mod tests {
assert!(!app.should_quit());
}
// More integration tests will be added here
#[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
}
}
}

View file

@ -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<usize> {
self.history_index
}
#[cfg(test)]
pub fn set_history_index(&mut self, index: Option<usize>) {
self.history_index = index;
}
#[cfg(test)]
pub fn command_history(&self) -> &VecDeque<String> {
&self.command_history
}
}
pub async fn run_tui<U, P>(user_service: U, product_service: P) -> anyhow::Result<()>