use super::env_overrides::parse_env_bool;
use super::*;
use crate::config::types::DeploymentMode;
use crate::error::ConfigError;
use std::env;
use std::ffi::OsString;
use std::sync::{Mutex, OnceLock};
fn with_locked_env(test: impl FnOnce()) {
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock poisoned");
test();
}
struct ScopedEnvVar {
key: &'static str,
previous: Option<OsString>,
}
impl ScopedEnvVar {
fn set(key: &'static str, value: &str) -> Self {
let previous = env::var_os(key);
env::set_var(key, value);
Self { key, previous }
}
}
impl Drop for ScopedEnvVar {
fn drop(&mut self) {
match self.previous.take() {
Some(previous) => env::set_var(self.key, previous),
None => env::remove_var(self.key),
}
}
}
fn scraper_config() -> Config {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.business.product_description = "A test product".to_string();
config.business.industry_topics = vec!["testing".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.provider_backend = "scraper".to_string();
config
}
fn x_api_config() -> Config {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.business.product_description = "A test product".to_string();
config.business.industry_topics = vec!["testing".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.client_id = "test-client-id".to_string();
config.x_api.provider_backend = "x_api".to_string();
config
}
#[test]
fn validate_scraper_backend_allows_empty_client_id() {
let config = scraper_config();
assert!(config.validate().is_ok());
}
#[test]
fn validate_x_api_backend_requires_client_id() {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.provider_backend = "x_api".to_string();
let errors = config.validate().unwrap_err();
assert!(errors
.iter()
.any(|e| matches!(e, ConfigError::MissingField { field } if field == "x_api.client_id")));
}
#[test]
fn validate_empty_backend_requires_client_id() {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.provider_backend = String::new();
let errors = config.validate().unwrap_err();
assert!(errors
.iter()
.any(|e| matches!(e, ConfigError::MissingField { field } if field == "x_api.client_id")));
}
#[test]
fn validate_x_api_with_client_id_passes() {
let config = x_api_config();
assert!(config.validate().is_ok());
}
#[test]
fn validate_cloud_scraper_rejected() {
let mut config = scraper_config();
config.deployment_mode = DeploymentMode::Cloud;
let errors = config.validate().unwrap_err();
assert!(errors.iter().any(
|e| matches!(e, ConfigError::InvalidValue { field, .. } if field == "x_api.provider_backend")
));
}
#[test]
fn validate_desktop_scraper_allowed() {
let mut config = scraper_config();
config.deployment_mode = DeploymentMode::Desktop;
assert!(config.validate().is_ok());
}
#[test]
fn validate_self_host_scraper_allowed() {
let mut config = scraper_config();
config.deployment_mode = DeploymentMode::SelfHost;
assert!(config.validate().is_ok());
}
#[test]
fn validate_invalid_backend_value_rejected() {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.provider_backend = "magic".to_string();
let errors = config.validate().unwrap_err();
assert!(errors
.iter()
.any(|e| matches!(e, ConfigError::InvalidValue { field, message }
if field == "x_api.provider_backend" && message.contains("magic"))));
}
#[test]
fn validate_scraper_allow_mutations_default_false() {
let config = Config::default();
assert!(!config.x_api.scraper_allow_mutations);
}
#[test]
fn env_var_override_provider_backend() {
with_locked_env(|| {
let _backend = ScopedEnvVar::set("TUITBOT_X_API__PROVIDER_BACKEND", "scraper");
let mut config = Config::default();
config.apply_env_overrides().expect("env override");
assert_eq!(config.x_api.provider_backend, "scraper");
});
}
#[test]
fn env_var_override_scraper_allow_mutations() {
with_locked_env(|| {
let _mutations = ScopedEnvVar::set("TUITBOT_X_API__SCRAPER_ALLOW_MUTATIONS", "true");
let mut config = Config::default();
config.apply_env_overrides().expect("env override");
assert!(config.x_api.scraper_allow_mutations);
});
}
#[test]
fn env_var_scraper_allow_mutations_false() {
with_locked_env(|| {
let _mutations = ScopedEnvVar::set("TUITBOT_X_API__SCRAPER_ALLOW_MUTATIONS", "false");
let mut config = Config::default();
config.apply_env_overrides().expect("env override");
assert!(!config.x_api.scraper_allow_mutations);
});
}
#[test]
fn env_var_scraper_allow_mutations_invalid() {
with_locked_env(|| {
let _mutations = ScopedEnvVar::set("TUITBOT_X_API__SCRAPER_ALLOW_MUTATIONS", "maybe");
let mut config = Config::default();
let result = config.apply_env_overrides();
assert!(result.is_err());
});
}
#[test]
fn scraper_config_toml_roundtrip() {
let toml_str = r#"
[x_api]
provider_backend = "scraper"
scraper_allow_mutations = true
[business]
product_name = "TestApp"
product_keywords = ["test"]
[llm]
provider = "ollama"
"#;
let config: Config = toml::from_str(toml_str).expect("valid TOML");
assert_eq!(config.x_api.provider_backend, "scraper");
assert!(config.x_api.scraper_allow_mutations);
assert!(config.x_api.client_id.is_empty());
let toml_out = toml::to_string_pretty(&config).expect("serialize");
let roundtripped: Config = toml::from_str(&toml_out).expect("re-parse");
assert_eq!(roundtripped.x_api.provider_backend, "scraper");
assert!(roundtripped.x_api.scraper_allow_mutations);
}
#[test]
fn x_api_config_toml_roundtrip() {
let toml_str = r#"
[x_api]
client_id = "my-client-id"
client_secret = "my-secret"
provider_backend = "x_api"
[business]
product_name = "TestApp"
product_keywords = ["test"]
[llm]
provider = "ollama"
"#;
let config: Config = toml::from_str(toml_str).expect("valid TOML");
assert_eq!(config.x_api.provider_backend, "x_api");
assert_eq!(config.x_api.client_id, "my-client-id");
assert!(!config.x_api.scraper_allow_mutations);
let toml_out = toml::to_string_pretty(&config).expect("serialize");
let roundtripped: Config = toml::from_str(&toml_out).expect("re-parse");
assert_eq!(roundtripped.x_api.client_id, "my-client-id");
assert_eq!(roundtripped.x_api.provider_backend, "x_api");
}
#[test]
fn provider_backend_defaults_to_empty_string() {
let config = Config::default();
assert_eq!(config.x_api.provider_backend, "");
}
#[test]
fn settings_json_includes_backend_fields() {
let mut config = scraper_config();
config.x_api.scraper_allow_mutations = true;
let json = serde_json::to_value(&config).expect("serialize");
let x_api = &json["x_api"];
assert_eq!(x_api["provider_backend"], "scraper");
assert_eq!(x_api["scraper_allow_mutations"], true);
assert_eq!(x_api["client_id"], "");
}
#[test]
fn settings_json_roundtrip_scraper_mode() {
let mut config = scraper_config();
config.x_api.scraper_allow_mutations = true;
let json = serde_json::to_value(&config).expect("serialize to JSON");
let toml_str = toml::to_string_pretty(&config).expect("serialize to TOML");
let from_toml: Config = toml::from_str(&toml_str).expect("parse TOML");
assert_eq!(from_toml.x_api.provider_backend, "scraper");
assert!(from_toml.x_api.scraper_allow_mutations);
let json_str = serde_json::to_string(&json).expect("json string");
let from_json: serde_json::Value = serde_json::from_str(&json_str).expect("parse json");
assert_eq!(from_json["x_api"]["provider_backend"], "scraper");
}
#[test]
fn parse_env_bool_accepts_variants() {
assert!(parse_env_bool("TEST", "true").unwrap());
assert!(parse_env_bool("TEST", "1").unwrap());
assert!(parse_env_bool("TEST", "yes").unwrap());
assert!(parse_env_bool("TEST", "YES").unwrap());
assert!(!parse_env_bool("TEST", "false").unwrap());
assert!(!parse_env_bool("TEST", "0").unwrap());
assert!(!parse_env_bool("TEST", "no").unwrap());
assert!(parse_env_bool("TEST", "maybe").is_err());
}