1pub fn resolve_api_key(default_env: &str, override_env: Option<&str>) -> Option<String> {
23 let env_var = override_env.unwrap_or(default_env);
24 match std::env::var(env_var) {
25 Ok(key) if !key.is_empty() => Some(key),
26 _ => None,
27 }
28}
29
30pub fn resolve_provider_key(
33 provider: &str,
34 default_env: &str,
35 override_env: Option<&str>,
36) -> Option<String> {
37 resolve_provider_key_in(
38 super::credentials::default_store(),
39 provider,
40 default_env,
41 override_env,
42 )
43}
44
45pub(crate) fn resolve_provider_key_in(
47 store: &dyn super::credentials::CredentialStore,
48 provider: &str,
49 default_env: &str,
50 override_env: Option<&str>,
51) -> Option<String> {
52 if override_env.is_some() {
53 return resolve_api_key(default_env, override_env);
57 }
58 resolve_api_key(default_env, None).or_else(|| store.get(provider))
59}
60
61pub fn resolve_provider_key_with_fallback(
63 provider: &str,
64 default_env: &str,
65 fallback_env: &str,
66 override_env: Option<&str>,
67) -> Option<String> {
68 if override_env.is_some() {
69 return resolve_api_key(default_env, override_env);
70 }
71 resolve_api_key_with_fallback(default_env, fallback_env, None)
72 .or_else(|| super::credentials::default_store().get(provider))
73}
74
75pub fn provider_key_source(
78 provider: &str,
79 default_env: &str,
80 override_env: Option<&str>,
81) -> &'static str {
82 if resolve_api_key(default_env, override_env).is_some() {
83 return "env";
84 }
85 if override_env.is_none() && super::credentials::default_store().get(provider).is_some() {
86 return "keyring";
87 }
88 "none"
89}
90
91pub fn resolve_api_key_with_fallback(
97 default_env: &str,
98 fallback_env: &str,
99 override_env: Option<&str>,
100) -> Option<String> {
101 if override_env.is_some() {
102 return resolve_api_key(default_env, override_env);
103 }
104 resolve_api_key(default_env, None).or_else(|| resolve_api_key(fallback_env, None))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn unique_env(prefix: &str) -> String {
117 use std::sync::atomic::{AtomicUsize, Ordering};
118 static N: AtomicUsize = AtomicUsize::new(0);
119 format!(
120 "{}_{}_{}",
121 prefix,
122 std::process::id(),
123 N.fetch_add(1, Ordering::SeqCst)
124 )
125 }
126
127 #[test]
128 fn provider_key_env_beats_keyring() {
129 use crate::utils::credentials::test_support::FakeStore;
130 let store = FakeStore::default();
131 store
132 .entries
133 .lock()
134 .unwrap()
135 .insert("groq".to_string(), "stored-key".to_string());
136 let var = unique_env("MERMAID_TEST_PK_ENV");
137 temp_env::with_var(&var, Some("env-key"), || {
138 assert_eq!(
139 resolve_provider_key_in(&store, "groq", &var, None),
140 Some("env-key".to_string()),
141 "env must have absolute precedence"
142 );
143 });
144 temp_env::with_var_unset(&var, || {
145 assert_eq!(
146 resolve_provider_key_in(&store, "groq", &var, None),
147 Some("stored-key".to_string()),
148 "keyring fills the gap when env is unset"
149 );
150 });
151 }
152
153 #[test]
154 fn override_env_blocks_keyring_fallback() {
155 use crate::utils::credentials::test_support::FakeStore;
156 let store = FakeStore::default();
157 store
158 .entries
159 .lock()
160 .unwrap()
161 .insert("groq".to_string(), "stored-key".to_string());
162 let default_var = unique_env("MERMAID_TEST_PK_DEFAULT");
163 let override_var = unique_env("MERMAID_TEST_PK_OVERRIDE");
164 temp_env::with_vars(
167 [
168 (default_var.as_str(), Some("default-key")),
169 (override_var.as_str(), None),
170 ],
171 || {
172 assert_eq!(
173 resolve_provider_key_in(&store, "groq", &default_var, Some(&override_var)),
174 None
175 );
176 },
177 );
178 }
179
180 #[test]
181 fn returns_none_when_env_var_unset() {
182 let var = unique_env("MERMAID_TEST_AUTH_UNSET");
183 temp_env::with_var_unset(&var, || {
184 assert_eq!(resolve_api_key(&var, None), None);
185 });
186 }
187
188 #[test]
189 fn returns_value_when_env_var_set() {
190 let var = unique_env("MERMAID_TEST_AUTH_SET");
191 temp_env::with_var(&var, Some("secret-value"), || {
192 assert_eq!(
193 resolve_api_key(&var, None),
194 Some("secret-value".to_string())
195 );
196 });
197 }
198
199 #[test]
200 fn empty_string_treated_as_unset() {
201 let var = unique_env("MERMAID_TEST_AUTH_EMPTY");
202 temp_env::with_var(&var, Some(""), || {
203 assert_eq!(resolve_api_key(&var, None), None);
204 });
205 }
206
207 #[test]
208 fn override_env_takes_precedence_over_default() {
209 let default_var = unique_env("MERMAID_TEST_AUTH_DEFAULT");
210 let override_var = unique_env("MERMAID_TEST_AUTH_OVERRIDE");
211 temp_env::with_vars(
212 [
213 (default_var.as_str(), Some("default-key")),
214 (override_var.as_str(), Some("override-key")),
215 ],
216 || {
217 let resolved = resolve_api_key(&default_var, Some(&override_var));
218 assert_eq!(resolved, Some("override-key".to_string()));
219 },
220 );
221 }
222
223 #[test]
224 fn override_env_unset_falls_through_to_none() {
225 let default_var = unique_env("MERMAID_TEST_AUTH_DEFAULT2");
230 let override_var = unique_env("MERMAID_TEST_AUTH_OVERRIDE2");
231 temp_env::with_vars(
232 [
233 (default_var.as_str(), Some("default-key")),
234 (override_var.as_str(), None),
235 ],
236 || {
237 let resolved = resolve_api_key(&default_var, Some(&override_var));
238 assert_eq!(resolved, None);
239 },
240 );
241 }
242
243 #[test]
244 fn fallback_env_used_only_when_default_is_unset() {
245 let default_var = unique_env("MERMAID_TEST_AUTH_FALLBACK_DEFAULT");
246 let fallback_var = unique_env("MERMAID_TEST_AUTH_FALLBACK_LEGACY");
247 temp_env::with_vars(
248 [
249 (default_var.as_str(), None),
250 (fallback_var.as_str(), Some("legacy-key")),
251 ],
252 || {
253 let resolved = resolve_api_key_with_fallback(&default_var, &fallback_var, None);
254 assert_eq!(resolved, Some("legacy-key".to_string()));
255 },
256 );
257
258 temp_env::with_vars(
259 [
260 (default_var.as_str(), Some("default-key")),
261 (fallback_var.as_str(), Some("legacy-key")),
262 ],
263 || {
264 let resolved = resolve_api_key_with_fallback(&default_var, &fallback_var, None);
265 assert_eq!(resolved, Some("default-key".to_string()));
266 },
267 );
268 }
269
270 #[test]
271 fn fallback_env_is_ignored_when_override_is_set() {
272 let default_var = unique_env("MERMAID_TEST_AUTH_OVERRIDE_DEFAULT3");
273 let fallback_var = unique_env("MERMAID_TEST_AUTH_OVERRIDE_FALLBACK3");
274 let override_var = unique_env("MERMAID_TEST_AUTH_OVERRIDE3");
275 temp_env::with_vars(
276 [
277 (default_var.as_str(), Some("default-key")),
278 (fallback_var.as_str(), Some("legacy-key")),
279 (override_var.as_str(), None),
280 ],
281 || {
282 let resolved =
283 resolve_api_key_with_fallback(&default_var, &fallback_var, Some(&override_var));
284 assert_eq!(resolved, None);
285 },
286 );
287 }
288}