#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use cc_switch::cli::cli::*;
use cc_switch::config::EnvironmentConfig;
use cc_switch::config::*;
use std::fs;
use tempfile::TempDir;
fn create_test_temp_dir() -> TempDir {
TempDir::new().expect("Failed to create temporary directory")
}
fn create_test_config(alias: &str, token: &str, url: &str) -> Configuration {
Configuration {
alias_name: alias.to_string(),
token: token.to_string(),
url: url.to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
}
}
#[test]
fn test_configuration_creation() {
let config = create_test_config("test", "sk-ant-test", "https://api.test.com");
assert_eq!(config.alias_name, "test");
assert_eq!(config.token, "sk-ant-test");
assert_eq!(config.url, "https://api.test.com");
}
#[test]
fn test_configuration_default() {
let config = Configuration::default();
assert_eq!(config.alias_name, "");
assert_eq!(config.token, "");
assert_eq!(config.url, "");
assert_eq!(config.model, None);
assert_eq!(config.small_fast_model, None);
}
#[test]
fn test_config_storage_default() {
let storage = ConfigStorage::default();
assert!(storage.configurations.is_empty());
}
#[test]
fn test_config_storage_add_configuration() {
let mut storage = ConfigStorage::default();
let config = create_test_config("test", "sk-ant-test", "https://api.test.com");
storage.add_configuration(config.clone());
assert_eq!(storage.configurations.len(), 1);
assert!(storage.configurations.contains_key("test"));
let stored_config = storage.configurations.get("test").unwrap();
assert_eq!(stored_config.alias_name, config.alias_name);
assert_eq!(stored_config.token, config.token);
assert_eq!(stored_config.url, config.url);
}
#[test]
fn test_config_storage_get_configuration() {
let mut storage = ConfigStorage::default();
let config = create_test_config("test", "sk-ant-test", "https://api.test.com");
assert!(storage.get_configuration("nonexistent").is_none());
storage.add_configuration(config);
let retrieved = storage.get_configuration("test").unwrap();
assert_eq!(retrieved.alias_name, "test");
assert_eq!(retrieved.token, "sk-ant-test");
assert_eq!(retrieved.url, "https://api.test.com");
}
#[test]
fn test_config_storage_remove_configuration() {
let mut storage = ConfigStorage::default();
let config = create_test_config("test", "sk-ant-test", "https://api.test.com");
storage.add_configuration(config);
assert!(storage.remove_configuration("test"));
assert!(!storage.configurations.contains_key("test"));
assert!(!storage.remove_configuration("nonexistent"));
}
#[test]
fn test_config_storage_save_and_load() {
let temp_dir = create_test_temp_dir();
let test_config_path = temp_dir.path().join("configurations.json");
let _test_get_config_storage_path =
|| Ok::<std::path::PathBuf, anyhow::Error>(test_config_path.clone());
let mut storage = ConfigStorage::default();
let config1 = create_test_config("config1", "sk-ant-test1", "https://api1.test.com");
let config2 = create_test_config("config2", "sk-ant-test2", "https://api2.test.com");
storage.add_configuration(config1);
storage.add_configuration(config2);
let json = serde_json::to_string_pretty(&storage).unwrap();
fs::write(&test_config_path, json).unwrap();
let loaded_content = fs::read_to_string(&test_config_path).unwrap();
let loaded_storage: ConfigStorage = serde_json::from_str(&loaded_content).unwrap();
assert_eq!(loaded_storage.configurations.len(), 2);
assert!(loaded_storage.configurations.contains_key("config1"));
assert!(loaded_storage.configurations.contains_key("config2"));
let loaded_config1 = loaded_storage.get_configuration("config1").unwrap();
assert_eq!(loaded_config1.alias_name, "config1");
assert_eq!(loaded_config1.token, "sk-ant-test1");
assert_eq!(loaded_config1.url, "https://api1.test.com");
}
#[test]
fn test_config_storage_load_nonexistent_file() {
let temp_dir = create_test_temp_dir();
let test_config_path = temp_dir.path().join("nonexistent.json");
let result = if test_config_path.exists() {
let content = fs::read_to_string(&test_config_path).unwrap();
serde_json::from_str::<ConfigStorage>(&content).unwrap()
} else {
ConfigStorage::default()
};
assert!(result.configurations.is_empty());
}
#[test]
fn test_config_storage_add_overwrites_existing() {
let mut storage = ConfigStorage::default();
let config1 = create_test_config("test", "sk-ant-test1", "https://api1.test.com");
let config2 = create_test_config("test", "sk-ant-test2", "https://api2.test.com");
storage.add_configuration(config1);
storage.add_configuration(config2);
assert_eq!(storage.configurations.len(), 1);
let stored_config = storage.get_configuration("test").unwrap();
assert_eq!(stored_config.token, "sk-ant-test2");
assert_eq!(stored_config.url, "https://api2.test.com");
}
#[test]
fn test_config_storage_multiple_configurations() {
let mut storage = ConfigStorage::default();
for i in 0..10 {
let config = create_test_config(
&format!("config{}", i),
&format!("sk-ant-test{}", i),
&format!("https://api{}.test.com", i),
);
storage.add_configuration(config);
}
assert_eq!(storage.configurations.len(), 10);
for i in 0..10 {
let alias = &format!("config{}", i);
assert!(storage.configurations.contains_key(alias));
let config = storage.get_configuration(alias).unwrap();
assert_eq!(config.token, format!("sk-ant-test{}", i));
assert_eq!(config.url, format!("https://api{}.test.com", i));
}
}
#[test]
fn test_environment_config_default() {
let env_config = EnvironmentConfig::default();
assert!(env_config.env_vars.is_empty());
}
#[test]
fn test_environment_config_from_config() {
let config = create_test_config("test", "sk-ant-test", "https://api.test.com");
let env_config = EnvironmentConfig::from_config(&config);
assert_eq!(env_config.env_vars.len(), 2);
assert_eq!(
env_config.env_vars.get("ANTHROPIC_AUTH_TOKEN"),
Some(&"sk-ant-test".to_string())
);
assert_eq!(
env_config.env_vars.get("ANTHROPIC_BASE_URL"),
Some(&"https://api.test.com".to_string())
);
}
#[test]
fn test_environment_config_with_models() {
let mut config = create_test_config("test", "sk-ant-test", "https://api.test.com");
config.model = Some("claude-3-5-sonnet-20241022".to_string());
config.small_fast_model = Some("claude-3-haiku-20240307".to_string());
let env_config = EnvironmentConfig::from_config(&config);
assert_eq!(env_config.env_vars.len(), 4);
assert_eq!(
env_config.env_vars.get("ANTHROPIC_MODEL"),
Some(&"claude-3-5-sonnet-20241022".to_string())
);
assert_eq!(
env_config.env_vars.get("ANTHROPIC_SMALL_FAST_MODEL"),
Some(&"claude-3-haiku-20240307".to_string())
);
}
#[test]
fn test_environment_config_empty() {
let env_config = EnvironmentConfig::empty();
assert!(env_config.env_vars.is_empty());
}
#[test]
fn test_environment_config_as_env_tuples() {
let config = create_test_config("test", "sk-ant-test", "https://api.test.com");
let env_config = EnvironmentConfig::from_config(&config);
let tuples = env_config.as_env_tuples();
assert_eq!(tuples.len(), 2);
assert!(tuples.contains(&(
"ANTHROPIC_AUTH_TOKEN".to_string(),
"sk-ant-test".to_string()
)));
assert!(tuples.contains(&(
"ANTHROPIC_BASE_URL".to_string(),
"https://api.test.com".to_string()
)));
}
#[test]
fn test_validate_alias_name_valid() {
assert!(validate_alias_name("test").is_ok());
assert!(validate_alias_name("my-config").is_ok());
assert!(validate_alias_name("config_123").is_ok());
}
#[test]
fn test_validate_alias_name_empty() {
let result = validate_alias_name("");
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Alias name cannot be empty"
);
}
#[test]
fn test_validate_alias_name_reserved_cc() {
let result = validate_alias_name("cc");
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Alias name 'cc' is reserved and cannot be used"
);
}
#[test]
fn test_validate_alias_name_whitespace() {
let result = validate_alias_name("test config");
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Alias name cannot contain whitespace"
);
}
#[test]
fn test_cli_parsing() {
use clap::Parser;
let args = vec![
"cc-switch",
"add",
"my-config",
"sk-ant-test",
"https://api.test.com",
];
let cli = Cli::try_parse_from(args).unwrap();
if let Some(Commands::Add {
alias_name,
token,
url,
force,
interactive,
token_arg,
url_arg,
..
}) = cli.command
{
assert_eq!(alias_name.unwrap(), "my-config");
assert_eq!(token, None);
assert_eq!(url, None);
assert!(!force);
assert!(!interactive);
assert_eq!(token_arg, Some("sk-ant-test".to_string()));
assert_eq!(url_arg, Some("https://api.test.com".to_string()));
} else {
panic!("Expected Add command");
}
}
#[test]
fn test_cli_parsing_with_flags() {
use clap::Parser;
let args = vec![
"cc-switch",
"add",
"my-config",
"-t",
"sk-ant-test",
"-u",
"https://api.test.com",
"-f",
"-i",
];
let cli = Cli::try_parse_from(args).unwrap();
if let Some(Commands::Add {
alias_name,
token,
url,
force,
interactive,
token_arg,
url_arg,
..
}) = cli.command
{
assert_eq!(alias_name.unwrap(), "my-config");
assert_eq!(token, Some("sk-ant-test".to_string()));
assert_eq!(url, Some("https://api.test.com".to_string()));
assert!(force);
assert!(interactive);
assert_eq!(token_arg, None);
assert_eq!(url_arg, None);
} else {
panic!("Expected Add command");
}
}
#[test]
fn test_cli_parsing_list_command() {
use clap::Parser;
let args = vec!["cc-switch", "list"];
let cli = Cli::try_parse_from(args).unwrap();
if let Some(Commands::List { plain: _ }) = cli.command {
} else {
panic!("Expected List command");
}
}
#[test]
fn test_cli_parsing_remove_command() {
use clap::Parser;
let args = vec!["cc-switch", "remove", "config1", "config2"];
let cli = Cli::try_parse_from(args).unwrap();
if let Some(Commands::Remove { alias_names }) = cli.command {
assert_eq!(alias_names, vec!["config1", "config2"]);
} else {
panic!("Expected Remove command");
}
}
#[test]
fn test_cli_parsing_completion_command() {
use clap::Parser;
let args = vec!["cc-switch", "completion", "fish"];
let cli = Cli::try_parse_from(args).unwrap();
if let Some(Commands::Completion { shell }) = cli.command {
assert_eq!(shell, "fish");
} else {
panic!("Expected Completion command");
}
}
#[test]
fn test_get_config_storage_path() {
let result = get_config_storage_path();
assert!(
result.is_ok(),
"Should be able to determine config storage path"
);
let path = result.unwrap();
assert!(path.to_string_lossy().contains(".claude"));
assert!(
path.to_string_lossy()
.contains("cc_auto_switch_setting.json")
);
}
#[test]
fn test_config_storage_load_create_directory() {
let temp_dir = create_test_temp_dir();
let _test_config_path = temp_dir.path().join("configurations.json");
let storage = ConfigStorage::load();
match storage {
Ok(s) => {
println!(
"Loaded storage with {} configurations",
s.configurations.len()
);
}
Err(_) => {
}
}
}
#[test]
fn test_config_storage_save_and_load_integration() {
let mut storage = ConfigStorage::default();
let config1 = create_test_config("save-test-1", "sk-ant-save-1", "https://save1.test.com");
let config2 = create_test_config("save-test-2", "sk-ant-save-2", "https://save2.test.com");
storage.add_configuration(config1);
storage.add_configuration(config2);
assert_eq!(storage.configurations.len(), 2);
assert!(storage.configurations.contains_key("save-test-1"));
assert!(storage.configurations.contains_key("save-test-2"));
let retrieved1 = storage.get_configuration("save-test-1").unwrap();
assert_eq!(retrieved1.token, "sk-ant-save-1");
assert_eq!(retrieved1.url, "https://save1.test.com");
let retrieved2 = storage.get_configuration("save-test-2").unwrap();
assert_eq!(retrieved2.token, "sk-ant-save-2");
assert_eq!(retrieved2.url, "https://save2.test.com");
}
#[test]
fn test_config_storage_remove_multiple() {
let mut storage = ConfigStorage::default();
for i in 0..5 {
let config = create_test_config(
&format!("remove-test-{}", i),
&format!("sk-ant-remove-{}", i),
&format!("https://remove-{}.test.com", i),
);
storage.add_configuration(config);
}
assert_eq!(storage.configurations.len(), 5);
assert!(storage.remove_configuration("remove-test-1"));
assert!(storage.remove_configuration("remove-test-3"));
assert_eq!(storage.configurations.len(), 3);
assert!(!storage.remove_configuration("non-existent"));
assert_eq!(storage.configurations.len(), 3);
assert!(storage.configurations.contains_key("remove-test-0"));
assert!(storage.configurations.contains_key("remove-test-2"));
assert!(storage.configurations.contains_key("remove-test-4"));
assert!(!storage.configurations.contains_key("remove-test-1"));
assert!(!storage.configurations.contains_key("remove-test-3"));
}
#[test]
fn test_environment_config_empty_model_strings() {
let mut config =
create_test_config("empty-model-test", "sk-ant-empty", "https://empty.test.com");
config.model = Some("".to_string()); config.small_fast_model = Some("".to_string());
let env_config = EnvironmentConfig::from_config(&config);
let env_tuples = env_config.as_env_tuples();
assert_eq!(env_tuples.len(), 2);
assert!(env_tuples.iter().any(|(k, _)| k == "ANTHROPIC_AUTH_TOKEN"));
assert!(env_tuples.iter().any(|(k, _)| k == "ANTHROPIC_BASE_URL"));
assert!(!env_tuples.iter().any(|(k, _)| k == "ANTHROPIC_MODEL"));
assert!(
!env_tuples
.iter()
.any(|(k, _)| k == "ANTHROPIC_SMALL_FAST_MODEL")
);
}
#[test]
fn test_environment_config_from_config_edge_cases() {
let config = Configuration {
alias_name: "edge-case".to_string(),
token: "".to_string(),
url: "".to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let env_config = EnvironmentConfig::from_config(&config);
let env_tuples = env_config.as_env_tuples();
assert_eq!(env_tuples.len(), 2);
assert!(
env_tuples
.iter()
.any(|(k, v)| k == "ANTHROPIC_AUTH_TOKEN" && v.is_empty())
);
assert!(
env_tuples
.iter()
.any(|(k, v)| k == "ANTHROPIC_BASE_URL" && v.is_empty())
);
}
#[test]
fn test_environment_config_partial_models() {
let mut config1 =
create_test_config("partial-1", "sk-ant-partial-1", "https://partial1.test.com");
config1.model = Some("claude-main-only".to_string());
let env_config1 = EnvironmentConfig::from_config(&config1);
let env_tuples1 = env_config1.as_env_tuples();
assert_eq!(env_tuples1.len(), 3);
assert!(
env_tuples1
.iter()
.any(|(k, v)| k == "ANTHROPIC_MODEL" && v == "claude-main-only")
);
assert!(
!env_tuples1
.iter()
.any(|(k, _)| k == "ANTHROPIC_SMALL_FAST_MODEL")
);
let mut config2 =
create_test_config("partial-2", "sk-ant-partial-2", "https://partial2.test.com");
config2.small_fast_model = Some("haiku-only".to_string());
let env_config2 = EnvironmentConfig::from_config(&config2);
let env_tuples2 = env_config2.as_env_tuples();
assert_eq!(env_tuples2.len(), 3);
assert!(!env_tuples2.iter().any(|(k, _)| k == "ANTHROPIC_MODEL"));
assert!(
env_tuples2
.iter()
.any(|(k, v)| k == "ANTHROPIC_SMALL_FAST_MODEL" && v == "haiku-only")
);
}
#[test]
fn test_validate_alias_name_edge_cases() {
assert!(
validate_alias_name("test\tconfig").is_err(),
"Should reject tabs"
);
assert!(
validate_alias_name("test\nconfig").is_err(),
"Should reject newlines"
);
assert!(
validate_alias_name("test\rconfig").is_err(),
"Should reject carriage returns"
);
assert!(
validate_alias_name("test config with multiple spaces").is_err(),
"Should reject multiple spaces"
);
assert!(
validate_alias_name("test-config").is_ok(),
"Should accept hyphens"
);
assert!(
validate_alias_name("test_config").is_ok(),
"Should accept underscores"
);
assert!(
validate_alias_name("test.config").is_ok(),
"Should accept dots"
);
assert!(
validate_alias_name("test123").is_ok(),
"Should accept numbers"
);
assert!(
validate_alias_name("123test").is_ok(),
"Should accept starting with numbers"
);
let long_alias = "a".repeat(1000);
assert!(
validate_alias_name(&long_alias).is_ok(),
"Should accept very long alias names"
);
assert!(
validate_alias_name("测试-config").is_ok(),
"Should accept unicode characters"
);
assert!(
validate_alias_name("αλιας").is_ok(),
"Should accept Greek characters"
);
assert!(
validate_alias_name("config-🚀").is_ok(),
"Should accept emoji characters"
);
}
#[test]
fn test_validate_alias_name_case_sensitivity() {
assert!(
validate_alias_name("CC").is_ok(),
"Should accept uppercase CC"
);
assert!(
validate_alias_name("Cc").is_ok(),
"Should accept mixed case Cc"
);
assert!(
validate_alias_name("cC").is_ok(),
"Should accept mixed case cC"
);
assert!(
validate_alias_name("cc-config").is_ok(),
"Should accept cc as prefix"
);
assert!(
validate_alias_name("config-cc").is_ok(),
"Should accept cc as suffix"
);
}
#[test]
fn test_configuration_serialization_format() {
let config = Configuration {
alias_name: "format-test".to_string(),
token: "sk-ant-format-test".to_string(),
url: "https://format.test.com".to_string(),
model: Some("claude-format-model".to_string()),
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let json = serde_json::to_string_pretty(&config).expect("Should serialize to pretty JSON");
assert!(json.contains("\"alias_name\": \"format-test\""));
assert!(json.contains("\"token\": \"sk-ant-format-test\""));
assert!(json.contains("\"url\": \"https://format.test.com\""));
assert!(json.contains("\"model\": \"claude-format-model\""));
assert!(!json.contains("small_fast_model"));
}
#[test]
fn test_configuration_deserialization_extra_fields() {
let json_with_extra_fields = r#"{
"alias_name": "extra-fields-test",
"token": "sk-ant-extra",
"url": "https://extra.test.com",
"model": "claude-extra-model",
"unknown_field": "should-be-ignored",
"another_unknown": 42
}"#;
let result: Result<Configuration, _> = serde_json::from_str(json_with_extra_fields);
assert!(
result.is_ok(),
"Should successfully deserialize despite extra fields"
);
let config = result.unwrap();
assert_eq!(config.alias_name, "extra-fields-test");
assert_eq!(config.token, "sk-ant-extra");
assert_eq!(config.url, "https://extra.test.com");
assert_eq!(config.model, Some("claude-extra-model".to_string()));
assert_eq!(config.small_fast_model, None);
}
#[test]
fn test_configuration_deserialization_missing_optional_fields() {
let minimal_json = r#"{
"alias_name": "minimal-test",
"token": "sk-ant-minimal",
"url": "https://minimal.test.com"
}"#;
let config: Configuration =
serde_json::from_str(minimal_json).expect("Should deserialize minimal JSON");
assert_eq!(config.alias_name, "minimal-test");
assert_eq!(config.token, "sk-ant-minimal");
assert_eq!(config.url, "https://minimal.test.com");
assert_eq!(config.model, None);
assert_eq!(config.small_fast_model, None);
}
#[test]
fn test_environment_config_as_env_tuples_order() {
let config = Configuration {
alias_name: "order-test".to_string(),
token: "sk-ant-order".to_string(),
url: "https://order.test.com".to_string(),
model: Some("claude-order-model".to_string()),
small_fast_model: Some("haiku-order-model".to_string()),
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let env_config = EnvironmentConfig::from_config(&config);
let env_tuples = env_config.as_env_tuples();
assert_eq!(env_tuples.len(), 4);
let var_names: Vec<String> = env_tuples.iter().map(|(k, _)| k.clone()).collect();
assert!(var_names.contains(&"ANTHROPIC_AUTH_TOKEN".to_string()));
assert!(var_names.contains(&"ANTHROPIC_BASE_URL".to_string()));
assert!(var_names.contains(&"ANTHROPIC_MODEL".to_string()));
assert!(var_names.contains(&"ANTHROPIC_SMALL_FAST_MODEL".to_string()));
for (key, value) in env_tuples {
match key.as_str() {
"ANTHROPIC_AUTH_TOKEN" => assert_eq!(value, "sk-ant-order"),
"ANTHROPIC_BASE_URL" => assert_eq!(value, "https://order.test.com"),
"ANTHROPIC_MODEL" => assert_eq!(value, "claude-order-model"),
"ANTHROPIC_SMALL_FAST_MODEL" => assert_eq!(value, "haiku-order-model"),
_ => panic!("Unexpected environment variable: {}", key),
}
}
}
#[test]
fn test_format_token_for_display_long_token() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "sk-ant-api03...STUVWXYZ");
}
#[test]
fn test_format_token_for_display_medium_token() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "sk-ant-medium1";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "sk-ant-***");
}
#[test]
fn test_format_token_for_display_short_token() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "short1";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "sho***");
}
#[test]
fn test_format_token_for_display_very_short_token() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "abc";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "ab***");
}
#[test]
fn test_format_token_for_display_single_char() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "x";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "x***");
}
#[test]
fn test_format_token_for_display_empty_token() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "***");
}
#[test]
fn test_format_token_for_display_boundary_exactly_20_chars() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "12345678901234567890";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "1234567890***");
}
#[test]
fn test_format_token_for_display_boundary_21_chars() {
use cc_switch::cli::display_utils::format_token_for_display;
let token = "123456789012345678901";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "123456789012...45678901");
}
}
#[cfg(test)]
mod config_edit_tests {
use cc_switch::config::EnvironmentConfig;
use cc_switch::config::types::{ConfigStorage, Configuration};
use tempfile::TempDir;
fn create_test_storage_dir() -> (TempDir, ConfigStorage) {
let temp_dir = TempDir::new().unwrap();
let mut storage = ConfigStorage::default();
let config = Configuration {
alias_name: "test-config".to_string(),
token: "sk-test-123".to_string(),
url: "https://api.test.com".to_string(),
model: Some("test-model".to_string()),
small_fast_model: Some("test-fast-model".to_string()),
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
storage.add_configuration(config);
(temp_dir, storage)
}
#[test]
fn test_update_configuration_same_alias() {
let (_temp_dir, mut storage) = create_test_storage_dir();
let updated_config = Configuration {
alias_name: "test-config".to_string(),
token: "sk-updated-456".to_string(),
url: "https://api.updated.com".to_string(),
model: Some("updated-model".to_string()),
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let result = storage.update_configuration("test-config", updated_config);
assert!(result.is_ok());
let config = storage.get_configuration("test-config").unwrap();
assert_eq!(config.token, "sk-updated-456");
assert_eq!(config.url, "https://api.updated.com");
assert_eq!(config.model, Some("updated-model".to_string()));
assert_eq!(config.small_fast_model, None);
}
#[test]
fn test_update_configuration_rename_alias() {
let (_temp_dir, mut storage) = create_test_storage_dir();
let renamed_config = Configuration {
alias_name: "renamed-config".to_string(),
token: "sk-test-123".to_string(),
url: "https://api.test.com".to_string(),
model: Some("test-model".to_string()),
small_fast_model: Some("test-fast-model".to_string()),
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let result = storage.update_configuration("test-config", renamed_config);
assert!(result.is_ok());
assert!(storage.get_configuration("test-config").is_none());
assert!(storage.get_configuration("renamed-config").is_some());
let config = storage.get_configuration("renamed-config").unwrap();
assert_eq!(config.alias_name, "renamed-config");
}
#[test]
fn test_update_configuration_nonexistent() {
let (_temp_dir, mut storage) = create_test_storage_dir();
let new_config = Configuration {
alias_name: "new-config".to_string(),
token: "sk-new-789".to_string(),
url: "https://api.new.com".to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let result = storage.update_configuration("nonexistent", new_config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn test_update_configuration_rename_to_existing() {
let (_temp_dir, mut storage) = create_test_storage_dir();
let config2 = Configuration {
alias_name: "config2".to_string(),
token: "sk-config2-456".to_string(),
url: "https://api.config2.com".to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
storage.add_configuration(config2);
let renamed_config = Configuration {
alias_name: "config2".to_string(),
token: "sk-overwritten".to_string(),
url: "https://api.overwritten.com".to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let result = storage.update_configuration("test-config", renamed_config);
assert!(result.is_ok());
assert!(storage.get_configuration("test-config").is_none());
let config = storage.get_configuration("config2").unwrap();
assert_eq!(config.token, "sk-overwritten");
assert_eq!(config.url, "https://api.overwritten.com");
}
#[test]
fn test_update_configuration_clear_optional_fields() {
let (_temp_dir, mut storage) = create_test_storage_dir();
let updated_config = Configuration {
alias_name: "test-config".to_string(),
token: "sk-test-123".to_string(),
url: "https://api.test.com".to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let result = storage.update_configuration("test-config", updated_config);
assert!(result.is_ok());
let config = storage.get_configuration("test-config").unwrap();
assert_eq!(config.model, None);
assert_eq!(config.small_fast_model, None);
}
#[test]
fn test_new_configuration_fields() {
let config = Configuration {
alias_name: "test".to_string(),
token: "sk-ant-test".to_string(),
url: "https://api.test.com".to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: Some(3000000),
claude_code_disable_nonessential_traffic: Some(1),
anthropic_default_sonnet_model: Some("MiniMax-M2".to_string()),
anthropic_default_opus_model: Some("MiniMax-M2".to_string()),
anthropic_default_haiku_model: Some("MiniMax-M2".to_string()),
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
assert_eq!(config.api_timeout_ms, Some(3000000));
assert_eq!(config.claude_code_disable_nonessential_traffic, Some(1));
assert_eq!(
config.anthropic_default_sonnet_model,
Some("MiniMax-M2".to_string())
);
assert_eq!(
config.anthropic_default_opus_model,
Some("MiniMax-M2".to_string())
);
assert_eq!(
config.anthropic_default_haiku_model,
Some("MiniMax-M2".to_string())
);
}
#[test]
fn test_environment_config_with_new_fields() {
let config = Configuration {
alias_name: "test".to_string(),
token: "sk-ant-test".to_string(),
url: "https://api.test.com".to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: Some(3000000),
claude_code_disable_nonessential_traffic: Some(1),
anthropic_default_sonnet_model: Some("MiniMax-M2".to_string()),
anthropic_default_opus_model: Some("MiniMax-M2".to_string()),
anthropic_default_haiku_model: Some("MiniMax-M2".to_string()),
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
};
let env_config = EnvironmentConfig::from_config(&config);
assert_eq!(
env_config.env_vars.get("API_TIMEOUT_MS"),
Some(&"3000000".to_string())
);
assert_eq!(
env_config
.env_vars
.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
Some(&"1".to_string())
);
assert_eq!(
env_config.env_vars.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
Some(&"MiniMax-M2".to_string())
);
assert_eq!(
env_config.env_vars.get("ANTHROPIC_DEFAULT_OPUS_MODEL"),
Some(&"MiniMax-M2".to_string())
);
assert_eq!(
env_config.env_vars.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
Some(&"MiniMax-M2".to_string())
);
}
}
#[cfg(test)]
mod claude_settings_tests {
use cc_switch::config::ClaudeSettings;
use cc_switch::config::types::StorageMode;
use std::collections::BTreeMap;
use std::fs;
use tempfile::TempDir;
fn create_test_temp_dir() -> TempDir {
TempDir::new().expect("Failed to create temporary directory")
}
fn create_test_config(
alias: &str,
token: &str,
url: &str,
) -> cc_switch::config::types::Configuration {
cc_switch::config::types::Configuration {
alias_name: alias.to_string(),
token: token.to_string(),
url: url.to_string(),
model: None,
small_fast_model: None,
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
claude_code_subagent_model: None,
claude_code_disable_nonstreaming_fallback: None,
claude_code_effort_level: None,
}
}
#[test]
fn test_switch_to_env_mode_detects_conflicts() {
let temp_dir = create_test_temp_dir();
let settings_path = temp_dir.path().join("settings.json");
let config = create_test_config("test-config", "sk-ant-test", "https://api.test.com");
let mut env = BTreeMap::new();
env.insert("ANTHROPIC_AUTH_TOKEN".to_string(), "old-token".to_string());
env.insert(
"ANTHROPIC_BASE_URL".to_string(),
"https://old-url.com".to_string(),
);
let mut settings = ClaudeSettings {
env,
other: BTreeMap::new(),
};
let result = settings.switch_to_config_with_mode(
&config,
StorageMode::Env,
Some(temp_dir.path().to_str().unwrap()),
);
assert!(
result.is_ok(),
"Switch to Env mode should succeed and auto-clean conflicts"
);
assert!(
settings_path.exists(),
"Settings file should be created after cleaning"
);
let content = fs::read_to_string(&settings_path).expect("Failed to read settings file");
let saved_settings: ClaudeSettings =
serde_json::from_str(&content).expect("Failed to parse saved settings");
assert!(
!saved_settings.env.contains_key("ANTHROPIC_AUTH_TOKEN"),
"ANTHROPIC_AUTH_TOKEN should be removed from env field"
);
assert!(
!saved_settings.env.contains_key("ANTHROPIC_BASE_URL"),
"ANTHROPIC_BASE_URL should be removed from env field"
);
}
#[test]
fn test_switch_to_env_mode_succeeds_without_conflicts() {
let temp_dir = create_test_temp_dir();
let settings_path = temp_dir.path().join("settings.json");
let config = create_test_config("test-config", "sk-ant-test", "https://api.test.com");
let mut other = BTreeMap::new();
other.insert(
"$schema".to_string(),
serde_json::Value::String("https://claude.ai/schema.json".to_string()),
);
other.insert(
"theme".to_string(),
serde_json::Value::String("dark".to_string()),
);
let mut settings = ClaudeSettings {
env: BTreeMap::new(),
other,
};
let result = settings.switch_to_config_with_mode(
&config,
StorageMode::Env,
Some(temp_dir.path().to_str().unwrap()),
);
assert!(
result.is_ok(),
"Switch to Env mode should succeed when no conflicts exist"
);
assert!(
!settings_path.exists(),
"Settings file should not be created in env mode"
);
}
#[test]
fn test_switch_to_config_mode_preserves_non_anthropic_fields() {
let temp_dir = create_test_temp_dir();
let settings_path = temp_dir.path().join("settings.json");
let config = create_test_config("test-config", "sk-ant-test", "https://api.test.com");
let mut other = BTreeMap::new();
other.insert(
"userPreferences".to_string(),
serde_json::Value::String("dark".to_string()),
);
other.insert(
"theme".to_string(),
serde_json::Value::String("dark".to_string()),
);
other.insert(
"anthropicAuthToken".to_string(),
serde_json::Value::String("old-token".to_string()),
);
let mut settings = ClaudeSettings {
env: BTreeMap::new(),
other,
};
let original_auth_token = std::env::var("ANTHROPIC_AUTH_TOKEN").ok();
let original_base_url = std::env::var("ANTHROPIC_BASE_URL").ok();
let original_model = std::env::var("ANTHROPIC_MODEL").ok();
let original_small_fast_model = std::env::var("ANTHROPIC_SMALL_FAST_MODEL").ok();
let original_disable_traffic =
std::env::var("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC").ok();
let original_sonnet = std::env::var("ANTHROPIC_DEFAULT_SONNET_MODEL").ok();
let original_opus = std::env::var("ANTHROPIC_DEFAULT_OPUS_MODEL").ok();
let original_haiku = std::env::var("ANTHROPIC_DEFAULT_HAIKU_MODEL").ok();
let original_experimental_teams =
std::env::var("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS").ok();
let original_disable_1m_context = std::env::var("CLAUDE_CODE_DISABLE_1M_CONTEXT").ok();
unsafe {
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_BASE_URL");
std::env::remove_var("ANTHROPIC_MODEL");
std::env::remove_var("ANTHROPIC_SMALL_FAST_MODEL");
std::env::remove_var("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC");
std::env::remove_var("ANTHROPIC_DEFAULT_SONNET_MODEL");
std::env::remove_var("ANTHROPIC_DEFAULT_OPUS_MODEL");
std::env::remove_var("ANTHROPIC_DEFAULT_HAIKU_MODEL");
std::env::remove_var("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS");
std::env::remove_var("CLAUDE_CODE_DISABLE_1M_CONTEXT");
}
let result = settings.switch_to_config_with_mode(
&config,
StorageMode::Config,
Some(temp_dir.path().to_str().unwrap()),
);
unsafe {
if let Some(token) = original_auth_token {
std::env::set_var("ANTHROPIC_AUTH_TOKEN", token);
} else {
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
}
if let Some(url) = original_base_url {
std::env::set_var("ANTHROPIC_BASE_URL", url);
} else {
std::env::remove_var("ANTHROPIC_BASE_URL");
}
if let Some(model) = original_model {
std::env::set_var("ANTHROPIC_MODEL", model);
} else {
std::env::remove_var("ANTHROPIC_MODEL");
}
if let Some(sfm) = original_small_fast_model {
std::env::set_var("ANTHROPIC_SMALL_FAST_MODEL", sfm);
} else {
std::env::remove_var("ANTHROPIC_SMALL_FAST_MODEL");
}
if let Some(disable) = original_disable_traffic {
std::env::set_var("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", disable);
} else {
std::env::remove_var("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC");
}
if let Some(sonnet) = original_sonnet {
std::env::set_var("ANTHROPIC_DEFAULT_SONNET_MODEL", sonnet);
} else {
std::env::remove_var("ANTHROPIC_DEFAULT_SONNET_MODEL");
}
if let Some(opus) = original_opus {
std::env::set_var("ANTHROPIC_DEFAULT_OPUS_MODEL", opus);
} else {
std::env::remove_var("ANTHROPIC_DEFAULT_OPUS_MODEL");
}
if let Some(haiku) = original_haiku {
std::env::set_var("ANTHROPIC_DEFAULT_HAIKU_MODEL", haiku);
} else {
std::env::remove_var("ANTHROPIC_DEFAULT_HAIKU_MODEL");
}
if let Some(experimental) = original_experimental_teams {
std::env::set_var("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", experimental);
} else {
std::env::remove_var("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS");
}
if let Some(disable_1m) = original_disable_1m_context {
std::env::set_var("CLAUDE_CODE_DISABLE_1M_CONTEXT", disable_1m);
} else {
std::env::remove_var("CLAUDE_CODE_DISABLE_1M_CONTEXT");
}
}
assert!(result.is_ok(), "Switch to Config mode should succeed");
let content = fs::read_to_string(&settings_path).expect("Failed to read settings file");
let saved_settings: ClaudeSettings =
serde_json::from_str(&content).expect("Failed to parse saved settings");
assert!(
saved_settings.env.contains_key("ANTHROPIC_AUTH_TOKEN"),
"Config mode should use UPPERCASE field names in 'env'"
);
assert_eq!(
saved_settings.env.get("ANTHROPIC_AUTH_TOKEN"),
Some(&"sk-ant-test".to_string())
);
assert!(
saved_settings.env.contains_key("ANTHROPIC_BASE_URL"),
"Config mode should have ANTHROPIC_BASE_URL in 'env'"
);
assert_eq!(
saved_settings.env.get("ANTHROPIC_BASE_URL"),
Some(&"https://api.test.com".to_string())
);
assert!(saved_settings.other.contains_key("userPreferences"));
assert!(saved_settings.other.contains_key("theme"));
}
#[test]
fn test_switch_to_env_mode_preserves_user_preference_fields() {
let temp_dir = create_test_temp_dir();
let settings_path = temp_dir.path().join("settings.json");
let config = create_test_config("test-config", "sk-ant-test", "https://api.test.com");
let mut env = BTreeMap::new();
env.insert("ANTHROPIC_AUTH_TOKEN".to_string(), "old-token".to_string());
env.insert(
"ANTHROPIC_BASE_URL".to_string(),
"https://old-url.com".to_string(),
);
env.insert(
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC".to_string(),
"1".to_string(),
);
env.insert(
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
"1".to_string(),
);
env.insert(
"CLAUDE_CODE_DISABLE_1M_CONTEXT".to_string(),
"1".to_string(),
);
let mut settings = ClaudeSettings {
env,
other: BTreeMap::new(),
};
let result = settings.switch_to_config_with_mode(
&config,
StorageMode::Env,
Some(temp_dir.path().to_str().unwrap()),
);
assert!(
result.is_ok(),
"Switch to Env mode should succeed"
);
let content = fs::read_to_string(&settings_path).expect("Failed to read settings file");
let saved_settings: ClaudeSettings =
serde_json::from_str(&content).expect("Failed to parse saved settings");
assert!(
!saved_settings.env.contains_key("ANTHROPIC_AUTH_TOKEN"),
"ANTHROPIC_AUTH_TOKEN should be removed from env field"
);
assert!(
!saved_settings.env.contains_key("ANTHROPIC_BASE_URL"),
"ANTHROPIC_BASE_URL should be removed from env field"
);
assert!(
saved_settings
.env
.contains_key("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC should be preserved"
);
assert_eq!(
saved_settings
.env
.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
Some(&"1".to_string()),
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC value should be preserved"
);
assert!(
saved_settings
.env
.contains_key("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"),
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS should be preserved"
);
assert_eq!(
saved_settings
.env
.get("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"),
Some(&"1".to_string()),
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS value should be preserved"
);
assert!(
saved_settings
.env
.contains_key("CLAUDE_CODE_DISABLE_1M_CONTEXT"),
"CLAUDE_CODE_DISABLE_1M_CONTEXT should be preserved"
);
assert_eq!(
saved_settings.env.get("CLAUDE_CODE_DISABLE_1M_CONTEXT"),
Some(&"1".to_string()),
"CLAUDE_CODE_DISABLE_1M_CONTEXT value should be preserved"
);
}
}