1pub fn resolve(name: &str, keychain_service: Option<&str>) -> anyhow::Result<String> {
2 if let Ok(v) = std::env::var(name) {
3 if !v.is_empty() {
4 return Ok(v);
5 }
6 }
7 #[cfg(target_os = "macos")]
8 if let Some(service) = keychain_service {
9 let out = std::process::Command::new("security")
10 .args(["find-generic-password", "-s", service, "-w"])
11 .output()?;
12 if out.status.success() {
13 let val = String::from_utf8(out.stdout)?.trim().to_string();
14 if !val.is_empty() {
15 return Ok(val);
16 }
17 }
18 }
19 #[cfg(not(target_os = "macos"))]
20 let _ = keychain_service;
21 anyhow::bail!("credential {:?} not found in environment or keychain", name);
22}
23
24#[cfg(test)]
25mod tests {
26 use super::*;
27 use std::sync::Mutex;
28
29 static ENV_LOCK: Mutex<()> = Mutex::new(());
30
31 #[test]
32 fn prefers_env_var() {
33 let _g = ENV_LOCK.lock().unwrap();
34 std::env::set_var("TEST_CRED_0038", "from-env");
35 let result = resolve("TEST_CRED_0038", Some("some-service"));
36 std::env::remove_var("TEST_CRED_0038");
37 assert_eq!(result.unwrap(), "from-env");
38 }
39
40 #[test]
41 fn empty_env_var_is_skipped() {
42 let _g = ENV_LOCK.lock().unwrap();
43 std::env::set_var("TEST_CRED_0038_EMPTY", "");
44 let result = resolve("TEST_CRED_0038_EMPTY", None);
45 std::env::remove_var("TEST_CRED_0038_EMPTY");
46 assert!(result.is_err());
47 }
48
49 #[test]
50 fn missing_credential_fails() {
51 let _g = ENV_LOCK.lock().unwrap();
52 std::env::remove_var("TEST_CRED_0038_MISSING");
53 let result = resolve("TEST_CRED_0038_MISSING", None);
54 assert!(result.is_err());
55 assert!(result.unwrap_err().to_string().contains("TEST_CRED_0038_MISSING"));
56 }
57}