Add missing TUI integration tests
This commit is contained in:
parent
8ef852beac
commit
ae5fb1cf31
2 changed files with 996 additions and 10 deletions
|
@ -9,17 +9,965 @@
|
|||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
#[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<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)
|
||||
}
|
||||
}
|
||||
|
||||
// More integration tests will be added here
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<()>
|
||||
|
|
Loading…
Add table
Reference in a new issue