use std::fmt;
use super::AuthCache;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenOrigin {
Env,
Cache,
}
pub struct ResolvedToken {
pub(crate) token: String,
pub origin: TokenOrigin,
}
impl fmt::Debug for ResolvedToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResolvedToken")
.field("token", &"[REDACTED]")
.field("origin", &self.origin)
.finish()
}
}
pub fn resolve_token(
env_token: Option<String>,
cache: &AuthCache,
server: &str,
) -> Option<ResolvedToken> {
let t = env_token.as_deref().map(str::trim).unwrap_or("");
if !t.is_empty() {
return Some(ResolvedToken {
token: t.to_owned(),
origin: TokenOrigin::Env,
});
}
if let Some(c) = cache.token(server) {
return Some(ResolvedToken {
token: c.to_owned(),
origin: TokenOrigin::Cache,
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const SERVER: &str = "https://api.dasch.swiss";
const OTHER_SERVER: &str = "https://api.stage.dasch.swiss";
const ENV_TOKEN: &str = "env-token-abc";
const CACHE_TOKEN: &str = "cache-token-xyz";
fn cache_with_token(server: &str, token: &str) -> AuthCache {
let mut cache = AuthCache::default();
cache.set_token(server.to_string(), token.to_string());
cache
}
#[test]
fn env_nonempty_wins_over_cache() {
let cache = cache_with_token(SERVER, CACHE_TOKEN);
let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
let rt = result.unwrap();
assert_eq!(rt.origin, TokenOrigin::Env);
assert_eq!(rt.token, ENV_TOKEN);
}
#[test]
fn env_whitespace_only_falls_through_to_cache() {
let cache = cache_with_token(SERVER, CACHE_TOKEN);
let result = resolve_token(Some(" ".to_string()), &cache, SERVER);
let rt = result.unwrap();
assert_eq!(rt.origin, TokenOrigin::Cache);
assert_eq!(rt.token, CACHE_TOKEN);
}
#[test]
fn env_whitespace_only_no_cache_returns_none() {
let cache = AuthCache::default();
let result = resolve_token(Some(" ".to_string()), &cache, SERVER);
assert!(result.is_none());
}
#[test]
fn env_empty_string_falls_through_to_cache() {
let cache = cache_with_token(SERVER, CACHE_TOKEN);
let result = resolve_token(Some(String::new()), &cache, SERVER);
let rt = result.unwrap();
assert_eq!(rt.origin, TokenOrigin::Cache);
assert_eq!(rt.token, CACHE_TOKEN);
}
#[test]
fn env_empty_string_no_cache_returns_none() {
let cache = AuthCache::default();
let result = resolve_token(Some(String::new()), &cache, SERVER);
assert!(result.is_none());
}
#[test]
fn env_none_uses_cache() {
let cache = cache_with_token(SERVER, CACHE_TOKEN);
let result = resolve_token(None, &cache, SERVER);
let rt = result.unwrap();
assert_eq!(rt.origin, TokenOrigin::Cache);
assert_eq!(rt.token, CACHE_TOKEN);
}
#[test]
fn env_set_cache_for_different_server_still_env() {
let cache = cache_with_token(OTHER_SERVER, CACHE_TOKEN);
let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
let rt = result.unwrap();
assert_eq!(rt.origin, TokenOrigin::Env);
assert_eq!(rt.token, ENV_TOKEN);
}
#[test]
fn env_set_empty_cache_returns_env() {
let cache = AuthCache::default();
let result = resolve_token(Some(ENV_TOKEN.to_string()), &cache, SERVER);
let rt = result.unwrap();
assert_eq!(rt.origin, TokenOrigin::Env);
assert_eq!(rt.token, ENV_TOKEN);
}
#[test]
fn both_absent_returns_none() {
let cache = AuthCache::default();
let result = resolve_token(None, &cache, SERVER);
assert!(result.is_none());
}
#[test]
fn debug_redacts_token() {
let secret = "super-secret-bearer-token";
let rt = ResolvedToken {
token: secret.to_string(),
origin: TokenOrigin::Env,
};
let rendered = format!("{rt:?}");
assert!(
!rendered.contains(secret),
"Debug impl leaked the token: {rendered}"
);
assert!(
rendered.contains("REDACTED"),
"expected redaction marker in Debug output, got: {rendered}"
);
}
}