use async_trait::async_trait;
use secrecy::SecretString;
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LivenessStatus {
Live,
Revoked,
Expired,
Throttled,
NotImplemented,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LivenessResult {
pub status: LivenessStatus,
pub detail: Option<String>,
pub expires_at: Option<String>,
}
impl LivenessResult {
pub fn live(detail: impl Into<String>) -> Self {
Self {
status: LivenessStatus::Live,
detail: Some(detail.into()),
expires_at: None,
}
}
pub fn revoked(detail: impl Into<String>) -> Self {
Self {
status: LivenessStatus::Revoked,
detail: Some(detail.into()),
expires_at: None,
}
}
pub fn expired(expires_at: impl Into<String>) -> Self {
Self {
status: LivenessStatus::Expired,
detail: None,
expires_at: Some(expires_at.into()),
}
}
pub fn throttled(detail: impl Into<String>) -> Self {
Self {
status: LivenessStatus::Throttled,
detail: Some(detail.into()),
expires_at: None,
}
}
pub fn not_implemented(provider: impl Into<String>) -> Self {
Self {
status: LivenessStatus::NotImplemented,
detail: Some(format!(
"{} liveness probe is not implemented; format-only validation only",
provider.into()
)),
expires_at: None,
}
}
pub fn error(detail: impl Into<String>) -> Self {
Self {
status: LivenessStatus::Error,
detail: Some(detail.into()),
expires_at: None,
}
}
}
#[async_trait]
pub trait LivenessProbe: Send + Sync {
async fn test(&self, _token: &SecretString) -> Result<LivenessResult> {
Ok(LivenessResult::not_implemented(self.provider_name()))
}
fn provider_name(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
struct StubProvider;
#[async_trait]
impl LivenessProbe for StubProvider {
fn provider_name(&self) -> &str {
"stub"
}
}
#[test]
fn live_helper_sets_status_and_detail() {
let r = LivenessResult::live("alice");
assert_eq!(r.status, LivenessStatus::Live);
assert_eq!(r.detail.as_deref(), Some("alice"));
assert!(r.expires_at.is_none());
}
#[test]
fn revoked_helper_sets_status_and_detail() {
let r = LivenessResult::revoked("token revoked by user");
assert_eq!(r.status, LivenessStatus::Revoked);
assert_eq!(r.detail.as_deref(), Some("token revoked by user"));
}
#[test]
fn expired_helper_carries_expiry_timestamp() {
let r = LivenessResult::expired("2025-12-31");
assert_eq!(r.status, LivenessStatus::Expired);
assert_eq!(r.expires_at.as_deref(), Some("2025-12-31"));
}
#[test]
fn throttled_helper_holds_retry_after_detail() {
let r = LivenessResult::throttled("retry after 60s");
assert_eq!(r.status, LivenessStatus::Throttled);
assert!(r.detail.unwrap().contains("retry"));
}
#[test]
fn not_implemented_helper_includes_provider_name_in_detail() {
let r = LivenessResult::not_implemented("clickup");
assert_eq!(r.status, LivenessStatus::NotImplemented);
let detail = r.detail.unwrap();
assert!(detail.contains("clickup"));
assert!(detail.contains("not implemented"));
}
#[test]
fn error_helper_sets_status_and_detail() {
let r = LivenessResult::error("connection refused");
assert_eq!(r.status, LivenessStatus::Error);
assert_eq!(r.detail.as_deref(), Some("connection refused"));
}
#[tokio::test]
async fn default_test_impl_returns_not_implemented_with_provider_name() {
let p = StubProvider;
let r = p
.test(&SecretString::from("any-token".to_owned()))
.await
.unwrap();
assert_eq!(r.status, LivenessStatus::NotImplemented);
let detail = r.detail.unwrap();
assert!(detail.contains("stub"));
}
#[tokio::test]
async fn default_impl_works_through_dyn_trait_object() {
let p: Box<dyn LivenessProbe> = Box::new(StubProvider);
assert_eq!(p.provider_name(), "stub");
let r = p.test(&SecretString::from("x".to_owned())).await.unwrap();
assert_eq!(r.status, LivenessStatus::NotImplemented);
}
}