use git_iris::common::CommonParams;
use git_iris::config::Config;
use git_iris::providers::ProviderConfig;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::sync::{Mutex, MutexGuard, OnceLock};
#[path = "test_utils.rs"]
mod test_utils;
use test_utils::{MockDataBuilder, setup_git_repo};
fn cwd_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(())).lock().expect("lock")
}
fn is_git_repo(dir: &Path) -> bool {
let status = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(dir)
.output()
.expect("Failed to execute git command");
status.status.success()
}
#[test]
fn test_project_config_security() {
let _guard = cwd_lock();
let (temp_dir, _git_repo) = setup_git_repo();
let original_dir = env::current_dir().expect("Failed to get current directory");
env::set_current_dir(temp_dir.path()).expect("Failed to change to test directory");
assert!(
is_git_repo(Path::new(".")),
"Current directory is not a git repository"
);
let mut config = MockDataBuilder::config();
for provider_name in &["openai", "anthropic", "google"] {
let provider_config = ProviderConfig {
api_key: format!("secret_{provider_name}_api_key"),
model: format!("{provider_name}_model"),
..Default::default()
};
config
.providers
.insert((*provider_name).to_string(), provider_config);
}
config
.save_as_project_config()
.expect("Failed to save project config");
let config_path = Path::new(".irisconfig");
assert!(config_path.exists(), "Project config file not created");
let content = fs::read_to_string(config_path).expect("Failed to read project config file");
for provider_name in &["openai", "anthropic", "google"] {
let api_key = format!("secret_{provider_name}_api_key");
assert!(
!content.contains(&api_key),
"API key was found in project config file"
);
}
let mut personal_config =
MockDataBuilder::test_config_with_api_key("openai", "personal_api_key");
personal_config
.providers
.get_mut("openai")
.expect("OpenAI provider should exist")
.model = "gpt-5.4".to_string();
let mut project_config = MockDataBuilder::config();
let project_provider_config = ProviderConfig {
api_key: String::new(), model: "gpt-5.4".to_string(),
..Default::default()
};
project_config
.providers
.insert("openai".to_string(), project_provider_config);
personal_config.merge_with_project_config(project_config);
let provider_config = personal_config
.providers
.get("openai")
.expect("OpenAI provider config not found");
assert_eq!(
provider_config.api_key, "personal_api_key",
"Personal API key was lost during merge"
);
assert_eq!(
provider_config.model, "gpt-5.4",
"Project model setting was not applied"
);
let common = CommonParams {
provider: Some("openai".to_string()),
model: None,
instructions: Some("Test instructions".to_string()),
preset: Some("default".to_string()),
gitmoji: Some(true),
gitmoji_flag: false,
no_gitmoji: false,
repository_url: None,
};
let mut config = MockDataBuilder::config();
common
.apply_to_config(&mut config)
.expect("Failed to apply common params");
let provider_config = config
.providers
.get_mut("openai")
.expect("OpenAI provider config not found");
provider_config.api_key = "cli_integration_api_key".to_string();
config
.save_as_project_config()
.expect("Failed to save project config with CLI params");
let content = fs::read_to_string(config_path).expect("Failed to read project config file");
assert!(
!content.contains("cli_integration_api_key"),
"API key from CLI integration was found in project config file"
);
env::set_current_dir(original_dir).expect("Failed to restore original directory");
}
#[test]
fn test_project_config_only_serializes_changed_values() {
let config = Config {
default_provider: String::new(),
providers: HashMap::new(),
use_gitmoji: false, instructions: String::new(),
instruction_preset: "conventional".to_string(), theme: String::new(),
subagent_timeout_secs: 120,
temp_instructions: None,
temp_preset: None,
is_project_config: true,
gitmoji_override: None,
};
let content = toml::to_string_pretty(&config).expect("Failed to serialize config");
assert!(
content.contains("use_gitmoji = false"),
"use_gitmoji should be serialized when false. Got:\n{content}"
);
assert!(
content.contains(r#"instruction_preset = "conventional""#),
"instruction_preset should be serialized when non-default. Got:\n{content}"
);
assert!(
!content.contains("default_provider"),
"default_provider should NOT be serialized when empty. Got:\n{content}"
);
assert!(
!content.contains("theme"),
"theme should NOT be serialized when empty. Got:\n{content}"
);
assert!(
!content.contains("subagent_timeout"),
"subagent_timeout should NOT be serialized when default (120). Got:\n{content}"
);
assert!(
!content.contains("instructions ="),
"empty instructions should NOT be serialized. Got:\n{content}"
);
assert!(
!content.contains("[providers]"),
"empty providers table should NOT be serialized. Got:\n{content}"
);
}
#[test]
fn test_project_config_with_provider_only_serializes_set_fields() {
let mut providers = HashMap::new();
providers.insert(
"anthropic".to_string(),
ProviderConfig {
api_key: String::new(),
model: "claude-opus-4-6".to_string(),
fast_model: None,
token_limit: None,
additional_params: HashMap::new(),
},
);
let config = Config {
default_provider: "anthropic".to_string(),
providers,
use_gitmoji: true, instructions: String::new(),
instruction_preset: "default".to_string(), theme: String::new(),
subagent_timeout_secs: 120,
temp_instructions: None,
temp_preset: None,
is_project_config: true,
gitmoji_override: None,
};
let content = toml::to_string_pretty(&config).expect("Failed to serialize config");
assert!(
content.contains(r#"default_provider = "anthropic""#),
"default_provider should be serialized when set. Got:\n{content}"
);
assert!(
content.contains("[providers.anthropic]"),
"provider section should exist. Got:\n{content}"
);
assert!(
content.contains(r#"model = "claude-opus-4-6""#),
"model should be serialized. Got:\n{content}"
);
assert!(
!content.contains("use_gitmoji"),
"use_gitmoji=true (default) should NOT be serialized. Got:\n{content}"
);
assert!(
!content.contains("instruction_preset"),
"instruction_preset=default should NOT be serialized. Got:\n{content}"
);
assert!(
!content.contains("theme"),
"empty theme should NOT be serialized. Got:\n{content}"
);
assert!(
!content.contains("api_key"),
"empty api_key should NOT be serialized. Got:\n{content}"
);
assert!(
!content.contains("fast_model"),
"None fast_model should NOT be serialized. Got:\n{content}"
);
assert!(
!content.contains("token_limit"),
"None token_limit should NOT be serialized. Got:\n{content}"
);
}
#[test]
fn test_provider_config_skip_serialization() {
let config = ProviderConfig {
api_key: String::new(),
model: String::new(),
fast_model: None,
token_limit: None,
additional_params: HashMap::new(),
};
let serialized = toml::to_string(&config).expect("Failed to serialize");
assert!(
!serialized.contains("api_key"),
"empty api_key should not serialize"
);
assert!(
!serialized.contains("model"),
"empty model should not serialize"
);
assert!(
!serialized.contains("fast_model"),
"None fast_model should not serialize"
);
assert!(
!serialized.contains("token_limit"),
"None token_limit should not serialize"
);
assert!(
!serialized.contains("additional_params"),
"empty additional_params should not serialize"
);
}
#[test]
fn test_provider_config_serializes_set_values() {
let mut params = HashMap::new();
params.insert("temperature".to_string(), "0.7".to_string());
let config = ProviderConfig {
api_key: String::new(), model: "gpt-5.4".to_string(),
fast_model: Some("gpt-5.4-mini".to_string()),
token_limit: Some(4096),
additional_params: params,
};
let serialized = toml::to_string(&config).expect("Failed to serialize");
assert!(
serialized.contains(r#"model = "gpt-5.4""#),
"model should serialize"
);
assert!(
serialized.contains(r#"fast_model = "gpt-5.4-mini""#),
"fast_model should serialize"
);
assert!(
serialized.contains("token_limit = 4096"),
"token_limit should serialize"
);
assert!(
serialized.contains("temperature"),
"additional_params should serialize"
);
assert!(
!serialized.contains("api_key"),
"empty api_key should not serialize"
);
}