use std::fmt::{self, Debug};
use std::time::SystemTime;
use async_trait::async_trait;
use boxlite_shared::errors::BoxliteResult;
#[derive(Clone)]
pub struct AccessToken {
pub token: String,
pub expires_at: Option<SystemTime>,
}
impl Debug for AccessToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AccessToken")
.field("token", &"[REDACTED]")
.field("expires_at", &self.expires_at)
.finish()
}
}
#[async_trait]
pub trait Credential: Send + Sync + Debug {
async fn get_token(&self) -> BoxliteResult<AccessToken>;
}
pub struct ApiKeyCredential {
key: String,
}
impl ApiKeyCredential {
pub fn new(key: impl Into<String>) -> Self {
Self { key: key.into() }
}
}
#[async_trait]
impl Credential for ApiKeyCredential {
async fn get_token(&self) -> BoxliteResult<AccessToken> {
Ok(AccessToken {
token: self.key.clone(),
expires_at: None,
})
}
}
impl Debug for ApiKeyCredential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApiKeyCredential")
.field("key", &"[REDACTED]")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
#[tokio::test]
async fn api_key_never_expires() {
let cred = ApiKeyCredential::new("blk_live_x");
let tok = cred.get_token().await.expect("get_token");
assert_eq!(tok.token, "blk_live_x");
assert!(tok.expires_at.is_none(), "API keys must not carry expiry");
}
#[test]
fn debug_redacts_key() {
let cred = ApiKeyCredential::new("super-secret-key");
let dbg = format!("{:?}", cred);
assert!(!dbg.contains("super-secret-key"), "Debug leaked key");
assert!(dbg.contains("REDACTED"));
}
#[test]
fn access_token_debug_redacts() {
let tok = AccessToken {
token: "leak-me".into(),
expires_at: None,
};
let dbg = format!("{:?}", tok);
assert!(!dbg.contains("leak-me"), "Debug leaked token");
}
#[derive(Debug)]
struct RotatingMock {
calls: AtomicUsize,
}
#[async_trait]
impl Credential for RotatingMock {
async fn get_token(&self) -> BoxliteResult<AccessToken> {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
Ok(AccessToken {
token: format!("tok-{n}"),
expires_at: Some(SystemTime::now() + Duration::from_secs(1)),
})
}
}
#[tokio::test]
async fn trait_object_dispatch() {
let cred: Arc<dyn Credential> = Arc::new(RotatingMock {
calls: AtomicUsize::new(0),
});
let a = cred.get_token().await.unwrap();
let b = cred.get_token().await.unwrap();
assert_eq!(a.token, "tok-0");
assert_eq!(b.token, "tok-1");
assert!(a.expires_at.is_some());
}
}