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