1use std::fmt;
7
8use super::AuthCache;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TokenOrigin {
13 Env,
15 Cache,
17}
18
19pub struct ResolvedToken {
24 pub(crate) token: String,
34 pub origin: TokenOrigin,
36}
37
38impl fmt::Debug for ResolvedToken {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 f.debug_struct("ResolvedToken")
41 .field("token", &"[REDACTED]")
42 .field("origin", &self.origin)
43 .finish()
44 }
45}
46
47pub fn resolve_token(
57 env_token: Option<String>,
58 cache: &AuthCache,
59 server: &str,
60) -> Option<ResolvedToken> {
61 let t = env_token.as_deref().map(str::trim).unwrap_or("");
62 if !t.is_empty() {
63 return Some(ResolvedToken {
64 token: t.to_owned(),
65 origin: TokenOrigin::Env,
66 });
67 }
68 if let Some(c) = cache.token(server) {
69 return Some(ResolvedToken {
70 token: c.to_owned(),
71 origin: TokenOrigin::Cache,
72 });
73 }
74 None
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 const SERVER: &str = "https://api.dasch.swiss";
82 const OTHER_SERVER: &str = "https://api.stage.dasch.swiss";
83 const ENV_TOKEN: &str = "env-token-abc";
84 const CACHE_TOKEN: &str = "cache-token-xyz";
85
86 fn cache_with_token(server: &str, token: &str) -> AuthCache {
87 let mut cache = AuthCache::default();
88 cache.set_token(server.to_string(), token.to_string());
89 cache
90 }
91
92 #[test]
94 fn env_nonempty_wins_over_cache() {
95 let cache = cache_with_token(SERVER, CACHE_TOKEN);
96 let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
97 let rt = result.unwrap();
98 assert_eq!(rt.origin, TokenOrigin::Env);
99 assert_eq!(rt.token, ENV_TOKEN);
100 }
101
102 #[test]
104 fn env_whitespace_only_falls_through_to_cache() {
105 let cache = cache_with_token(SERVER, CACHE_TOKEN);
106 let result = resolve_token(Some(" ".to_string()), &cache, SERVER);
107 let rt = result.unwrap();
108 assert_eq!(rt.origin, TokenOrigin::Cache);
109 assert_eq!(rt.token, CACHE_TOKEN);
110 }
111
112 #[test]
114 fn env_whitespace_only_no_cache_returns_none() {
115 let cache = AuthCache::default();
116 let result = resolve_token(Some(" ".to_string()), &cache, SERVER);
117 assert!(result.is_none());
118 }
119
120 #[test]
122 fn env_empty_string_falls_through_to_cache() {
123 let cache = cache_with_token(SERVER, CACHE_TOKEN);
124 let result = resolve_token(Some(String::new()), &cache, SERVER);
125 let rt = result.unwrap();
126 assert_eq!(rt.origin, TokenOrigin::Cache);
127 assert_eq!(rt.token, CACHE_TOKEN);
128 }
129
130 #[test]
132 fn env_empty_string_no_cache_returns_none() {
133 let cache = AuthCache::default();
134 let result = resolve_token(Some(String::new()), &cache, SERVER);
135 assert!(result.is_none());
136 }
137
138 #[test]
140 fn env_none_uses_cache() {
141 let cache = cache_with_token(SERVER, CACHE_TOKEN);
142 let result = resolve_token(None, &cache, SERVER);
143 let rt = result.unwrap();
144 assert_eq!(rt.origin, TokenOrigin::Cache);
145 assert_eq!(rt.token, CACHE_TOKEN);
146 }
147
148 #[test]
150 fn env_set_cache_for_different_server_still_env() {
151 let cache = cache_with_token(OTHER_SERVER, CACHE_TOKEN);
152 let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
153 let rt = result.unwrap();
154 assert_eq!(rt.origin, TokenOrigin::Env);
155 assert_eq!(rt.token, ENV_TOKEN);
156 }
157
158 #[test]
160 fn env_set_empty_cache_returns_env() {
161 let cache = AuthCache::default();
162 let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
163 let rt = result.unwrap();
164 assert_eq!(rt.origin, TokenOrigin::Env);
165 assert_eq!(rt.token, ENV_TOKEN);
166 }
167
168 #[test]
170 fn both_absent_returns_none() {
171 let cache = AuthCache::default();
172 let result = resolve_token(None, &cache, SERVER);
173 assert!(result.is_none());
174 }
175
176 #[test]
178 fn debug_redacts_token() {
179 let secret = "super-secret-bearer-token";
180 let rt = ResolvedToken {
181 token: secret.to_string(),
182 origin: TokenOrigin::Env,
183 };
184 let rendered = format!("{rt:?}");
185 assert!(
186 !rendered.contains(secret),
187 "Debug impl leaked the token: {rendered}"
188 );
189 assert!(
190 rendered.contains("REDACTED"),
191 "expected redaction marker in Debug output, got: {rendered}"
192 );
193 }
194}