use robotnik::setup::{Args, make_config}; use serial_test::serial; use std::{fs, path::PathBuf}; use tempfile::TempDir; /// Helper to create a temporary config file fn create_config_file(dir: &TempDir, content: &str) -> PathBuf { let config_path = dir.path().join("config.toml"); fs::write(&config_path, content).unwrap(); config_path } /// Helper to parse config using environment and config file async fn parse_config_from_file(config_path: &PathBuf) -> config::Config { config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) .build() .unwrap() } #[tokio::test] #[serial] async fn test_setup_make_config_overrides() { let temp = TempDir::new().unwrap(); let config_content = "\ api-key = \"file-key\" model = \"file-model\" port = 6667 "; let config_path = create_config_file(&temp, config_content); // Construct Args with overrides let args = Args { api_key: Some("cli-key".to_string()), base_url: None, /* Should fail if required and not in file/env? No, base-url is optional * in args */ chroot_dir: None, command_dir: None, instruct: None, model: None, // Should fallback to file channels: None, config_file: Some(config_path), server: None, // Should use default or file? Args has default "irc.libera.chat" port: Some("9999".to_string()), nickname: None, nick_password: None, username: None, use_tls: None, }; let config = make_config(args).expect("Failed to make config"); // Check overrides assert_eq!(config.get_string("api-key").unwrap(), "cli-key"); assert_eq!(config.get_string("port").unwrap(), "9999"); assert_eq!(config.get_int("port").unwrap(), 9999); // Check fallback to file assert_eq!(config.get_string("model").unwrap(), "file-model"); } #[tokio::test] async fn test_config_file_loads_all_settings() { let temp = TempDir::new().unwrap(); let config_content = "\ api-key = \"test-api-key-123\" base-url = \"https://api.test.com\" chroot-dir = \"/test/chroot\" command-path = \"/test/commands\" model = \"test-model\" instruct = \"Test instructions\" server = \"test.irc.server\" port = 6667 channels = [\"#test1\", \"#test2\"] username = \"testuser\" nickname = \"testnick\" use-tls = false "; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; // Verify all settings are loaded correctly assert_eq!(config.get_string("api-key").unwrap(), "test-api-key-123"); assert_eq!( config.get_string("base-url").unwrap(), "https://api.test.com" ); assert_eq!(config.get_string("chroot-dir").unwrap(), "/test/chroot"); assert_eq!(config.get_string("command-path").unwrap(), "/test/commands"); assert_eq!(config.get_string("model").unwrap(), "test-model"); assert_eq!(config.get_string("instruct").unwrap(), "Test instructions"); assert_eq!(config.get_string("server").unwrap(), "test.irc.server"); assert_eq!(config.get_int("port").unwrap(), 6667); let channels: Vec = config.get("channels").unwrap(); assert_eq!(channels, vec!["#test1", "#test2"]); assert_eq!(config.get_string("username").unwrap(), "testuser"); assert_eq!(config.get_string("nickname").unwrap(), "testnick"); assert_eq!(config.get_bool("use-tls").unwrap(), false); } #[tokio::test] async fn test_config_file_partial_settings() { let temp = TempDir::new().unwrap(); // Only provide required settings let config_content = "\ api-key = \"minimal-key\" base-url = \"https://minimal.api.com\" model = \"minimal-model\" server = \"minimal.server\" port = 6697 channels = [\"#minimal\"] "; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; // Verify required settings are loaded assert_eq!(config.get_string("api-key").unwrap(), "minimal-key"); assert_eq!( config.get_string("base-url").unwrap(), "https://minimal.api.com" ); assert_eq!(config.get_string("model").unwrap(), "minimal-model"); // Verify optional settings are not present assert!(config.get_string("chroot-dir").is_err()); assert!(config.get_string("instruct").is_err()); assert!(config.get_string("username").is_err()); } #[tokio::test] #[serial] async fn test_config_with_environment_variables() { // NOTE: This test documents a limitation in setup.rs // setup.rs uses Environment::with_prefix("BOT") without a separator // This means BOT_API_KEY maps to "api_key", NOT "api-key" // Since config.toml uses kebab-case, environment variables won't override properly // This is a known issue in the current implementation let temp = TempDir::new().unwrap(); let config_content = "\ api_key = \"file-api-key\" base_url = \"https://file.api.com\" model = \"file-model\" "; let config_path = create_config_file(&temp, config_content); // Set environment variables (with BOT_ prefix as setup.rs uses) unsafe { std::env::set_var("BOT_API_KEY", "env-api-key"); std::env::set_var("BOT_MODEL", "env-model"); } let config = config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) .add_source(config::Environment::with_prefix("BOT")) .build() .unwrap(); // Environment variables should override file settings (when using underscore keys) assert_eq!(config.get_string("api_key").unwrap(), "env-api-key"); assert_eq!(config.get_string("model").unwrap(), "env-model"); // File setting should be used when no env var assert_eq!( config.get_string("base_url").unwrap(), "https://file.api.com" ); // Cleanup unsafe { std::env::remove_var("BOT_API_KEY"); std::env::remove_var("BOT_MODEL"); } } #[tokio::test] async fn test_command_line_overrides_config_file() { let temp = TempDir::new().unwrap(); let config_content = "\ api-key = \"file-api-key\" base-url = \"https://file.api.com\" model = \"file-model\" server = \"file.server\" port = 6667 channels = [\"#file\"] nickname = \"filenick\" username = \"fileuser\" "; let config_path = create_config_file(&temp, config_content); // Simulate command-line arguments overriding config file let config = config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) .set_override_option("api-key", Some("cli-api-key".to_string())) .unwrap() .set_override_option("model", Some("cli-model".to_string())) .unwrap() .set_override_option("server", Some("cli.server".to_string())) .unwrap() .set_override_option("nickname", Some("clinick".to_string())) .unwrap() .build() .unwrap(); // Command-line values should override file settings assert_eq!(config.get_string("api-key").unwrap(), "cli-api-key"); assert_eq!(config.get_string("model").unwrap(), "cli-model"); assert_eq!(config.get_string("server").unwrap(), "cli.server"); assert_eq!(config.get_string("nickname").unwrap(), "clinick"); // Non-overridden values should come from file assert_eq!( config.get_string("base-url").unwrap(), "https://file.api.com" ); assert_eq!(config.get_string("username").unwrap(), "fileuser"); assert_eq!(config.get_int("port").unwrap(), 6667); } #[tokio::test] #[serial] async fn test_command_line_overrides_environment_and_file() { let temp = TempDir::new().unwrap(); let config_content = "\ api_key = \"file-api-key\" model = \"file-model\" base_url = \"https://file.api.com\" "; let config_path = create_config_file(&temp, config_content); // Set environment variable unsafe { std::env::set_var("BOT_API_KEY", "env-api-key"); } // Build config with all three sources let config = config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) .add_source(config::Environment::with_prefix("BOT")) .set_override_option("api_key", Some("cli-api-key".to_string())) .unwrap() .build() .unwrap(); // Command-line should win over both environment and file assert_eq!(config.get_string("api_key").unwrap(), "cli-api-key"); // Cleanup unsafe { std::env::remove_var("BOT_API_KEY"); } } #[tokio::test] #[serial] async fn test_precedence_order() { // Test: CLI > Environment > Config File > Defaults // Using underscore keys to match how setup.rs actually works let temp = TempDir::new().unwrap(); let config_content = "\ api_key = \"file-key\" base_url = \"https://file-url.com\" model = \"file-model\" server = \"file-server\" "; let config_path = create_config_file(&temp, config_content); // Set environment variables unsafe { std::env::set_var("BOT_BASE_URL", "https://env-url.com"); std::env::set_var("BOT_MODEL", "env-model"); } let config = config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) .add_source(config::Environment::with_prefix("BOT")) .set_override_option("model", Some("cli-model".to_string())) .unwrap() .build() .unwrap(); // CLI overrides everything assert_eq!(config.get_string("model").unwrap(), "cli-model"); // Environment overrides file assert_eq!( config.get_string("base_url").unwrap(), "https://env-url.com" ); // File is used when no env or CLI assert_eq!(config.get_string("api_key").unwrap(), "file-key"); assert_eq!(config.get_string("server").unwrap(), "file-server"); // Cleanup unsafe { std::env::remove_var("BOT_BASE_URL"); std::env::remove_var("BOT_MODEL"); } } #[tokio::test] async fn test_boolean_use_tls_setting() { let temp = TempDir::new().unwrap(); // Test with use-tls = true (kebab-case as in config.toml) let config_content_true = r#" use-tls = true "#; let config_path = create_config_file(&temp, config_content_true); let config = parse_config_from_file(&config_path).await; assert_eq!(config.get_bool("use-tls").unwrap(), true); // Test with use-tls = false let config_content_false = r#" use-tls = false "#; let config_path = create_config_file(&temp, config_content_false); let config = parse_config_from_file(&config_path).await; assert_eq!(config.get_bool("use-tls").unwrap(), false); } #[tokio::test] async fn test_use_tls_naming_inconsistency() { // This test documents a bug: setup.rs uses "use_tls" (underscore) // but config.toml uses "use-tls" (kebab-case) let temp = TempDir::new().unwrap(); let config_content = r#" use-tls = true "#; let config_path = create_config_file(&temp, config_content); // Build config the way setup.rs does it let config = config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) // setup.rs line 119 uses "use_tls" (underscore) instead of "use-tls" (kebab) .set_override_option("use_tls", Some(false)) .unwrap() .build() .unwrap(); // This should read from the override (false), not the file (true) // But due to the naming mismatch, it might not work as expected // The config file uses "use-tls" but the override uses "use_tls" // With kebab-case (matches config.toml) assert_eq!(config.get_bool("use-tls").unwrap(), true); // With underscore (matches setup.rs override) assert_eq!(config.get_bool("use_tls").unwrap(), false); } #[tokio::test] async fn test_channels_as_array() { let temp = TempDir::new().unwrap(); let config_content = "\ channels = [\"#chan1\", \"#chan2\", \"#chan3\"] "; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; let channels: Vec = config.get("channels").unwrap(); assert_eq!(channels.len(), 3); assert_eq!(channels[0], "#chan1"); assert_eq!(channels[1], "#chan2"); assert_eq!(channels[2], "#chan3"); } #[tokio::test] async fn test_channels_override_from_cli() { let temp = TempDir::new().unwrap(); let config_content = "\ channels = [\"#file1\", \"#file2\"] "; let config_path = create_config_file(&temp, config_content); let cli_channels = vec![ "#cli1".to_string(), "#cli2".to_string(), "#cli3".to_string(), ]; let config = config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) .set_override_option("channels", Some(cli_channels.clone())) .unwrap() .build() .unwrap(); let channels: Vec = config.get("channels").unwrap(); assert_eq!(channels, cli_channels); assert_eq!(channels.len(), 3); } #[tokio::test] async fn test_port_as_integer() { let temp = TempDir::new().unwrap(); let config_content = r#" port = 6697 "#; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; // Port should be readable as both integer and string assert_eq!(config.get_int("port").unwrap(), 6697); assert_eq!(config.get_string("port").unwrap(), "6697"); } #[tokio::test] async fn test_port_override_from_cli_as_string() { // setup.rs passes port as Option from clap let temp = TempDir::new().unwrap(); let config_content = r#" port = 6667 "#; let config_path = create_config_file(&temp, config_content); let config = config::Config::builder() .add_source(config::File::with_name(&config_path.to_string_lossy()).required(true)) .set_override_option("port", Some("9999".to_string())) .unwrap() .build() .unwrap(); // CLI override should work assert_eq!(config.get_string("port").unwrap(), "9999"); assert_eq!(config.get_int("port").unwrap(), 9999); } #[tokio::test] async fn test_missing_required_fields_fails() { let temp = TempDir::new().unwrap(); // Create config without required api-key let config_content = r#" model = "test-model" "#; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; // Should fail when trying to get required field assert!(config.get_string("api-key").is_err()); } #[tokio::test] async fn test_optional_instruct_field() { let temp = TempDir::new().unwrap(); let config_content = r#" instruct = "Custom bot instructions" "#; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; assert_eq!( config.get_string("instruct").unwrap(), "Custom bot instructions" ); } #[tokio::test] async fn test_command_path_field() { // command-path is in config.toml but not used anywhere in the code let temp = TempDir::new().unwrap(); let config_content = r#" command-path = "/custom/commands" "#; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; assert_eq!( config.get_string("command-path").unwrap(), "/custom/commands" ); } #[tokio::test] async fn test_chroot_dir_field() { let temp = TempDir::new().unwrap(); let config_content = r#" chroot-dir = "/var/lib/bot/root" "#; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; assert_eq!( config.get_string("chroot-dir").unwrap(), "/var/lib/bot/root" ); } #[tokio::test] async fn test_empty_config_file() { let temp = TempDir::new().unwrap(); let config_content = ""; let config_path = create_config_file(&temp, config_content); // Should build successfully but have no values let config = parse_config_from_file(&config_path).await; assert!(config.get_string("api-key").is_err()); assert!(config.get_string("model").is_err()); } #[tokio::test] async fn test_all_cli_override_keys_match_config_format() { // This test documents which override keys in setup.rs match the config.toml format let temp = TempDir::new().unwrap(); let config_content = "\ api-key = \"test\" base-url = \"https://test.com\" chroot-dir = \"/test\" command-path = \"/cmds\" model = \"test-model\" instruct = \"test\" channels = [\"#test\"] server = \"test.server\" port = 6697 nickname = \"test\" username = \"test\" use-tls = true "; let config_path = create_config_file(&temp, config_content); let config = parse_config_from_file(&config_path).await; // All these should work with kebab-case (as in config.toml) assert!(config.get_string("api-key").is_ok()); assert!(config.get_string("base-url").is_ok()); assert!(config.get_string("chroot-dir").is_ok()); assert!(config.get_string("command-path").is_ok()); assert!(config.get_string("model").is_ok()); assert!(config.get_string("instruct").is_ok()); let channels_result: Result, _> = config.get("channels"); assert!(channels_result.is_ok()); assert!(config.get_string("server").is_ok()); assert!(config.get_int("port").is_ok()); assert!(config.get_string("nickname").is_ok()); assert!(config.get_string("username").is_ok()); assert!(config.get_bool("use-tls").is_ok()); }