#![cfg(feature = "backend-http")]
use std::collections::HashMap;
use proserpina::backend::credentials::{authed_configs, resolve_configs, Credentials};
use proserpina::backend::http::HttpConfig;
use proserpina::backend::roster::Provider;
#[test]
fn credentials_parse_a_minimal_single_provider() {
let toml = r#"
[deepseek]
api_key = "sk-abc"
"#;
let creds = Credentials::from_toml(toml).expect("valid toml");
let ds = creds.override_for("deepseek").expect("deepseek present");
assert_eq!(ds.api_key.as_deref(), Some("sk-abc"));
assert!(ds.model.is_none());
assert!(ds.base_url.is_none());
}
#[test]
fn credentials_parse_provider_with_model_and_base_url_overrides() {
let toml = r#"
[zai]
api_key = "k"
model = "glm-5.2"
base_url = "https://custom.example/v1"
"#;
let creds = Credentials::from_toml(toml).expect("valid toml");
let zai = creds.override_for("zai").expect("zai present");
assert_eq!(zai.api_key.as_deref(), Some("k"));
assert_eq!(zai.model.as_deref(), Some("glm-5.2"));
assert_eq!(zai.base_url.as_deref(), Some("https://custom.example/v1"));
}
#[test]
fn credentials_parse_multiple_providers_and_custom_entries() {
let toml = r#"
[deepseek]
api_key = "ds"
[my-local-llm]
base_url = "http://localhost:11434/v1"
model = "llama3"
api_key = "ollama"
"#;
let creds = Credentials::from_toml(toml).expect("valid toml");
assert!(creds.override_for("deepseek").is_some());
let custom = creds.override_for("my-local-llm").expect("custom present");
assert_eq!(
custom.base_url.as_deref(),
Some("http://localhost:11434/v1")
);
assert_eq!(custom.model.as_deref(), Some("llama3"));
assert_eq!(custom.api_key.as_deref(), Some("ollama"));
}
#[test]
fn credentials_from_str_errors_on_malformed_toml() {
let bad = r#"
[deepseek
api_key = missing-quotes
"#;
assert!(Credentials::from_toml(bad).is_err());
}
#[test]
fn empty_credentials_have_no_overrides() {
let creds = Credentials::from_toml("").expect("empty toml is valid");
assert!(creds.override_for("anything").is_none());
}
#[test]
fn from_path_reads_a_written_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("credentials.toml");
std::fs::write(
&path,
r#"
[deepseek]
api_key = "from-file"
[zai]
api_key = "k"
model = "glm-5.2"
"#,
)
.expect("write");
let creds = Credentials::from_path(&path).expect("file parses");
assert_eq!(
creds.override_for("deepseek").unwrap().api_key.as_deref(),
Some("from-file")
);
assert_eq!(
creds.override_for("zai").unwrap().model.as_deref(),
Some("glm-5.2")
);
}
#[test]
fn from_path_errors_on_a_nonexistent_path() {
let path = std::path::PathBuf::from("/nonexistent/proserpina/credentials.toml");
assert!(Credentials::from_path(&path).is_err());
}
#[test]
fn from_path_errors_on_a_malformed_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("bad.toml");
std::fs::write(&path, "[deepseek\napi_key = oops").expect("write");
assert!(Credentials::from_path(&path).is_err());
}
fn env(keys: &[(&str, &str)]) -> HashMap<String, String> {
keys.iter()
.copied()
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect()
}
fn registry() -> Vec<Provider> {
vec![
Provider::new("deepseek")
.with_base_url("https://api.deepseek.com/v1")
.with_model("deepseek-chat")
.with_key_env_var("DEEPSEEK_API_KEY"),
Provider::new("zai")
.with_base_url("https://open.bigmodel.cn/api/paas/v4")
.with_model("glm-4-plus")
.with_key_env_var("ZAI_API_KEY"),
]
}
fn find<'a>(configs: &'a [HttpConfig], model: &str) -> Option<&'a HttpConfig> {
configs.iter().find(|c| c.model == model)
}
#[test]
fn resolve_returns_only_providers_with_a_resolved_key() {
let env = env(&[("DEEPSEEK_API_KEY", "sk-env")]);
let creds = Credentials::default();
let configs = resolve_configs(®istry(), &creds, &env).expect("ok");
assert_eq!(configs.len(), 1);
assert_eq!(configs[0].model, "deepseek-chat");
assert_eq!(configs[0].api_key, "sk-env");
}
#[test]
fn resolve_env_key_takes_precedence_over_config_key() {
let env = env(&[("DEEPSEEK_API_KEY", "sk-env")]);
let creds = Credentials::from_toml(
r#"[deepseek]
api_key = "sk-config""#,
)
.unwrap();
let configs = resolve_configs(®istry(), &creds, &env).unwrap();
assert_eq!(configs[0].api_key, "sk-env");
}
#[test]
fn resolve_config_key_used_when_env_absent() {
let env = HashMap::new();
let creds = Credentials::from_toml(
r#"[deepseek]
api_key = "sk-config""#,
)
.unwrap();
let configs = resolve_configs(®istry(), &creds, &env).unwrap();
assert_eq!(configs[0].api_key, "sk-config");
}
#[test]
fn resolve_applies_config_model_override_over_registry_default() {
let env = env(&[("ZAI_API_KEY", "k")]);
let creds = Credentials::from_toml(
r#"[zai]
api_key = "k"
model = "glm-5.2""#,
)
.unwrap();
let configs = resolve_configs(®istry(), &creds, &env).unwrap();
let zai = find(&configs, "glm-5.2").expect("glm-5.2 should be present");
assert_eq!(zai.base_url, "https://open.bigmodel.cn/api/paas/v4");
}
#[test]
fn resolve_applies_config_base_url_override() {
let env = env(&[("DEEPSEEK_API_KEY", "k")]);
let creds = Credentials::from_toml(
r#"[deepseek]
api_key = "k"
base_url = "https://proxy.example/v1""#,
)
.unwrap();
let configs = resolve_configs(®istry(), &creds, &env).unwrap();
assert_eq!(configs[0].base_url, "https://proxy.example/v1");
}
#[test]
fn resolve_includes_custom_providers_not_in_the_registry() {
let env = HashMap::new();
let creds = Credentials::from_toml(
r#"[my-local-llm]
base_url = "http://localhost:11434/v1"
model = "llama3"
api_key = "ollama""#,
)
.unwrap();
let configs = resolve_configs(®istry(), &creds, &env).unwrap();
let custom = find(&configs, "llama3").expect("custom provider resolves");
assert_eq!(custom.base_url, "http://localhost:11434/v1");
assert_eq!(custom.api_key, "ollama");
}
#[test]
fn resolve_errors_when_a_custom_provider_is_missing_required_fields() {
let env = HashMap::new();
let creds = Credentials::from_toml(
r#"[my-broken]
api_key = "k""#,
)
.unwrap();
let result = resolve_configs(®istry(), &creds, &env);
let err = result.expect_err("missing fields -> error");
let msg = format!("{err}");
assert!(msg.contains("my-broken"), "error should name the provider");
assert!(msg.contains("model"));
assert!(msg.contains("base_url"));
}
#[test]
fn resolve_custom_provider_env_fallback_uses_uppercased_name() {
let env = env(&[("MY_LOCAL_LLM_API_KEY", "sk-env")]);
let creds = Credentials::from_toml(
r#"[my-local-llm]
base_url = "http://x/v1"
model = "m""#,
)
.unwrap();
let configs = resolve_configs(®istry(), &creds, &env).unwrap();
assert_eq!(configs[0].api_key, "sk-env");
}
#[test]
fn resolve_returns_empty_when_nothing_authed_and_no_custom() {
let env = HashMap::new();
let creds = Credentials::default();
let configs = resolve_configs(®istry(), &creds, &env).unwrap();
assert!(configs.is_empty());
}
#[test]
fn authed_configs_uses_resolve_over_real_registry_and_env() {
for var in [
"DEEPSEEK_API_KEY",
"OPENAI_API_KEY",
"MOONSHOT_API_KEY",
"DASHSCOPE_API_KEY",
"ZAI_API_KEY",
"GOOGLE_API_KEY",
] {
std::env::remove_var(var);
}
let temp_config = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(temp_config.path(), "# empty\n").expect("write");
std::env::set_var("PROSERPINA_CONFIG", temp_config.path());
std::env::set_var("PI_HOME", "/nonexistent/pi-test-home");
let configs = authed_configs().expect("empty is not an error");
std::env::remove_var("PROSERPINA_CONFIG");
std::env::remove_var("PI_HOME");
assert!(
configs.is_empty(),
"no keys + no config -> no authed configs"
);
}
#[test]
fn exclude_list_parsed_from_toml() {
let toml = r#"
exclude = ["qwen3.7-max", "mercury-2"]
"#;
let creds = Credentials::from_toml(toml).expect("valid toml");
let excludes = creds.exclude();
assert_eq!(excludes.len(), 2);
assert!(excludes.contains(&"qwen3.7-max".to_owned()));
assert!(excludes.contains(&"mercury-2".to_owned()));
}
#[test]
fn exclude_defaults_to_empty() {
let toml = "[deepseek]\napi_key = \"sk-x\"";
let creds = Credentials::from_toml(toml).expect("valid toml");
assert!(creds.exclude().is_empty());
}
#[test]
fn merge_model_creates_new_entry() {
let mut creds = Credentials::default();
creds.merge_model("openai", "gpt-5.5");
let ov = creds.override_for("openai").expect("entry created");
assert_eq!(ov.model.as_deref(), Some("gpt-5.5"));
assert!(ov.api_key.is_none()); }
#[test]
fn merge_model_preserves_existing_fields() {
let toml = r#"
[openai]
api_key = "sk-test"
oauth_access = "acc"
oauth_refresh = "ref"
oauth_expires = 9999999999999
"#;
let mut creds = Credentials::from_toml(toml).expect("valid toml");
creds.merge_model("openai", "gpt-5.5");
let ov = creds.override_for("openai").expect("entry exists");
assert_eq!(ov.model.as_deref(), Some("gpt-5.5"));
assert_eq!(ov.api_key.as_deref(), Some("sk-test"));
assert_eq!(ov.oauth_access.as_deref(), Some("acc"));
assert_eq!(ov.oauth_refresh.as_deref(), Some("ref"));
assert_eq!(ov.oauth_expires, Some(9999999999999));
}