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(env_token: Option<String>, cache: &AuthCache, server: &str) -> Option<ResolvedToken> {
57 let t = env_token.as_deref().map(str::trim).unwrap_or("");
58 if !t.is_empty() {
59 return Some(ResolvedToken { token: t.to_owned(), origin: TokenOrigin::Env });
60 }
61 if let Some(c) = cache.token(server) {
62 return Some(ResolvedToken { token: c.to_owned(), origin: TokenOrigin::Cache });
63 }
64 None
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 const SERVER: &str = "https://api.dasch.swiss";
72 const OTHER_SERVER: &str = "https://api.stage.dasch.swiss";
73 const ENV_TOKEN: &str = "env-token-abc";
74 const CACHE_TOKEN: &str = "cache-token-xyz";
75
76 fn cache_with_token(server: &str, token: &str) -> AuthCache {
77 let mut cache = AuthCache::default();
78 cache.set_token(server.to_string(), token.to_string());
79 cache
80 }
81
82 #[test]
84 fn env_nonempty_wins_over_cache() {
85 let cache = cache_with_token(SERVER, CACHE_TOKEN);
86 let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
87 let rt = result.unwrap();
88 assert_eq!(rt.origin, TokenOrigin::Env);
89 assert_eq!(rt.token, ENV_TOKEN);
90 }
91
92 #[test]
94 fn env_whitespace_only_falls_through_to_cache() {
95 let cache = cache_with_token(SERVER, CACHE_TOKEN);
96 let result = resolve_token(Some(" ".to_string()), &cache, SERVER);
97 let rt = result.unwrap();
98 assert_eq!(rt.origin, TokenOrigin::Cache);
99 assert_eq!(rt.token, CACHE_TOKEN);
100 }
101
102 #[test]
104 fn env_whitespace_only_no_cache_returns_none() {
105 let cache = AuthCache::default();
106 let result = resolve_token(Some(" ".to_string()), &cache, SERVER);
107 assert!(result.is_none());
108 }
109
110 #[test]
112 fn env_empty_string_falls_through_to_cache() {
113 let cache = cache_with_token(SERVER, CACHE_TOKEN);
114 let result = resolve_token(Some(String::new()), &cache, SERVER);
115 let rt = result.unwrap();
116 assert_eq!(rt.origin, TokenOrigin::Cache);
117 assert_eq!(rt.token, CACHE_TOKEN);
118 }
119
120 #[test]
122 fn env_empty_string_no_cache_returns_none() {
123 let cache = AuthCache::default();
124 let result = resolve_token(Some(String::new()), &cache, SERVER);
125 assert!(result.is_none());
126 }
127
128 #[test]
130 fn env_none_uses_cache() {
131 let cache = cache_with_token(SERVER, CACHE_TOKEN);
132 let result = resolve_token(None, &cache, SERVER);
133 let rt = result.unwrap();
134 assert_eq!(rt.origin, TokenOrigin::Cache);
135 assert_eq!(rt.token, CACHE_TOKEN);
136 }
137
138 #[test]
140 fn env_set_cache_for_different_server_still_env() {
141 let cache = cache_with_token(OTHER_SERVER, CACHE_TOKEN);
142 let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
143 let rt = result.unwrap();
144 assert_eq!(rt.origin, TokenOrigin::Env);
145 assert_eq!(rt.token, ENV_TOKEN);
146 }
147
148 #[test]
150 fn env_set_empty_cache_returns_env() {
151 let cache = AuthCache::default();
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 both_absent_returns_none() {
161 let cache = AuthCache::default();
162 let result = resolve_token(None, &cache, SERVER);
163 assert!(result.is_none());
164 }
165
166 #[test]
168 fn debug_redacts_token() {
169 let secret = "super-secret-bearer-token";
170 let rt = ResolvedToken { token: secret.to_string(), origin: TokenOrigin::Env };
171 let rendered = format!("{rt:?}");
172 assert!(!rendered.contains(secret), "Debug impl leaked the token: {rendered}");
173 assert!(
174 rendered.contains("REDACTED"),
175 "expected redaction marker in Debug output, got: {rendered}"
176 );
177 }
178}