use std::{ fs::{self, Permissions}, os::unix::fs::PermissionsExt, path::Path, time::Duration, }; use robotnik::command::CommandDir; use tempfile::TempDir; /// Helper to create executable test scripts fn create_command(dir: &Path, name: &str, script: &str) { let path = dir.join(name); fs::write(&path, script).unwrap(); fs::set_permissions(&path, Permissions::from_mode(0o755)).unwrap(); } /// Parse a bot message like "!weather 07008" into (command_name, argument) fn parse_bot_message(message: &str) -> Option<(&str, &str)> { if !message.starts_with('!') { return None; } let without_prefix = &message[1..]; let mut parts = without_prefix.splitn(2, ' '); let command = parts.next()?; let arg = parts.next().unwrap_or(""); Some((command, arg)) } #[tokio::test] async fn test_bot_message_finds_and_runs_command() { let temp = TempDir::new().unwrap(); // Create a weather command that echoes the zip code create_command( temp.path(), "weather", r#"#!/bin/bash echo "Weather for $1: Sunny, 72°F" "#, ); let cmd_dir = CommandDir::new(temp.path()); let message = "!weather 73135"; // Parse the message let (command_name, arg) = parse_bot_message(message).unwrap(); assert_eq!(command_name, "weather"); assert_eq!(arg, "73135"); // Find and run the command let result = cmd_dir.run_command(command_name, arg).await; assert!(result.is_ok()); let bytes = result.unwrap(); let output = String::from_utf8_lossy(&bytes); assert!(output.contains("Weather for 73135")); assert!(output.contains("Sunny")); } #[tokio::test] async fn test_bot_message_command_not_found() { let temp = TempDir::new().unwrap(); let cmd_dir = CommandDir::new(temp.path()); let message = "!nonexistent arg"; let (command_name, arg) = parse_bot_message(message).unwrap(); let result = cmd_dir.run_command(command_name, arg).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Not found")); } #[tokio::test] async fn test_bot_message_with_multiple_arguments() { let temp = TempDir::new().unwrap(); // Create a command that handles multiple words as a single argument create_command( temp.path(), "echo", r#"#!/bin/bash echo "You said: $1" "#, ); let cmd_dir = CommandDir::new(temp.path()); let message = "!echo hello world how are you"; let (command_name, arg) = parse_bot_message(message).unwrap(); assert_eq!(command_name, "echo"); assert_eq!(arg, "hello world how are you"); let result = cmd_dir.run_command(command_name, arg).await; assert!(result.is_ok()); let bytes = result.unwrap(); let output = String::from_utf8_lossy(&bytes); assert!(output.contains("hello world how are you")); } #[tokio::test] async fn test_bot_message_without_argument() { let temp = TempDir::new().unwrap(); create_command( temp.path(), "help", r#"#!/bin/bash echo "Available commands: weather, echo, help" "#, ); let cmd_dir = CommandDir::new(temp.path()); let message = "!help"; let (command_name, arg) = parse_bot_message(message).unwrap(); assert_eq!(command_name, "help"); assert_eq!(arg, ""); let result = cmd_dir.run_command(command_name, arg).await; assert!(result.is_ok()); let bytes = result.unwrap(); let output = String::from_utf8_lossy(&bytes); assert!(output.contains("Available commands")); } #[tokio::test] async fn test_bot_message_command_returns_error_exit_code() { let temp = TempDir::new().unwrap(); // Create a command that fails for invalid input create_command( temp.path(), "validate", r#"#!/bin/bash if [ -z "$1" ]; then echo "Error: Input required" >&2 exit 1 fi echo "Valid: $1" "#, ); let cmd_dir = CommandDir::new(temp.path()); let message = "!validate"; let (command_name, arg) = parse_bot_message(message).unwrap(); let result = cmd_dir.run_command(command_name, arg).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Error running")); } #[tokio::test] async fn test_bot_message_with_timeout() { let temp = TempDir::new().unwrap(); create_command( temp.path(), "quick", r#"#!/bin/bash echo "Result: $1" "#, ); let cmd_dir = CommandDir::new(temp.path()); let message = "!quick test"; let (command_name, arg) = parse_bot_message(message).unwrap(); let result = cmd_dir .run_command_with_timeout(command_name, arg, Duration::from_secs(5)) .await; assert!(result.is_ok()); } #[tokio::test] async fn test_bot_message_command_times_out() { let temp = TempDir::new().unwrap(); create_command( temp.path(), "slow", r#"#!/bin/bash sleep 10 echo "Done" "#, ); let cmd_dir = CommandDir::new(temp.path()); let message = "!slow arg"; let (command_name, arg) = parse_bot_message(message).unwrap(); let result = cmd_dir .run_command_with_timeout(command_name, arg, Duration::from_millis(100)) .await; assert!(result.is_err()); } #[tokio::test] async fn test_multiple_commands_in_directory() { let temp = TempDir::new().unwrap(); create_command( temp.path(), "weather", r#"#!/bin/bash echo "Weather: Sunny" "#, ); create_command( temp.path(), "time", r#"#!/bin/bash echo "Time: 12:00" "#, ); create_command( temp.path(), "joke", r#"#!/bin/bash echo "Why did the robot go on vacation? To recharge!" "#, ); let cmd_dir = CommandDir::new(temp.path()); // Test each command let messages = ["!weather", "!time", "!joke"]; let expected = ["Sunny", "12:00", "recharge"]; for (message, expect) in messages.iter().zip(expected.iter()) { let (command_name, arg) = parse_bot_message(message).unwrap(); let result = cmd_dir.run_command(command_name, arg).await; assert!(result.is_ok()); let bytes = result.unwrap(); let output = String::from_utf8_lossy(&bytes); assert!( output.contains(expect), "Expected '{}' in '{}'", expect, output ); } } #[tokio::test] async fn test_non_bot_message_ignored() { // Messages not starting with ! should be ignored let messages = ["hello world", "weather 73135", "?help", "/command", ""]; for message in messages { assert!( parse_bot_message(message).is_none(), "Should ignore: {}", message ); } } #[tokio::test] async fn test_command_output_is_bytes() { let temp = TempDir::new().unwrap(); // Create a command that outputs binary-safe content create_command( temp.path(), "binary", r#"#!/bin/bash printf "Hello\x00World" "#, ); let cmd_dir = CommandDir::new(temp.path()); let message = "!binary test"; let (command_name, arg) = parse_bot_message(message).unwrap(); let result = cmd_dir.run_command(command_name, arg).await; assert!(result.is_ok()); let output = result.unwrap(); // Should preserve the null byte assert_eq!(&output[..], b"Hello\x00World"); }