use std::time::{Duration, Instant};
use a3s_box_core::error::{BoxError, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReattestConfig {
pub enabled: bool,
#[serde(default = "default_interval")]
pub interval_secs: u64,
#[serde(default = "default_max_failures")]
pub max_failures: u32,
#[serde(default)]
pub failure_action: FailureAction,
#[serde(default = "default_grace_period")]
pub grace_period_secs: u64,
}
fn default_interval() -> u64 {
300
}
fn default_max_failures() -> u32 {
3
}
fn default_grace_period() -> u64 {
60
}
impl Default for ReattestConfig {
fn default() -> Self {
Self {
enabled: false,
interval_secs: 300,
max_failures: 3,
failure_action: FailureAction::default(),
grace_period_secs: 60,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum FailureAction {
#[default]
Warn,
Event,
Stop,
}
pub struct ReattestState {
config: ReattestConfig,
boot_time: Instant,
last_success: Option<Instant>,
last_check: Option<Instant>,
consecutive_failures: u32,
total_successes: u64,
total_failures: u64,
}
impl ReattestState {
pub fn new(config: ReattestConfig) -> Self {
Self {
config,
boot_time: Instant::now(),
last_success: None,
last_check: None,
consecutive_failures: 0,
total_successes: 0,
total_failures: 0,
}
}
pub fn is_check_due(&self) -> bool {
if !self.config.enabled {
return false;
}
if self.boot_time.elapsed() < Duration::from_secs(self.config.grace_period_secs) {
return false;
}
match self.last_check {
None => true, Some(last) => last.elapsed() >= Duration::from_secs(self.config.interval_secs),
}
}
pub fn record_success(&mut self) {
let now = Instant::now();
self.last_success = Some(now);
self.last_check = Some(now);
self.consecutive_failures = 0;
self.total_successes += 1;
}
pub fn record_failure(&mut self) -> FailureAction {
let now = Instant::now();
self.last_check = Some(now);
self.consecutive_failures += 1;
self.total_failures += 1;
if self.consecutive_failures >= self.config.max_failures {
self.config.failure_action
} else {
FailureAction::Warn
}
}
pub fn consecutive_failures(&self) -> u32 {
self.consecutive_failures
}
pub fn total_successes(&self) -> u64 {
self.total_successes
}
pub fn total_failures(&self) -> u64 {
self.total_failures
}
pub fn time_since_last_success(&self) -> Option<Duration> {
self.last_success.map(|t| t.elapsed())
}
pub fn time_since_last_check(&self) -> Option<Duration> {
self.last_check.map(|t| t.elapsed())
}
pub fn has_attested(&self) -> bool {
self.last_success.is_some()
}
pub fn is_failed(&self) -> bool {
self.consecutive_failures >= self.config.max_failures
}
pub fn failure_action(&self) -> FailureAction {
self.config.failure_action
}
pub fn summary(&self) -> ReattestSummary {
ReattestSummary {
enabled: self.config.enabled,
interval_secs: self.config.interval_secs,
consecutive_failures: self.consecutive_failures,
max_failures: self.config.max_failures,
total_successes: self.total_successes,
total_failures: self.total_failures,
is_failed: self.is_failed(),
failure_action: self.config.failure_action,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReattestSummary {
pub enabled: bool,
pub interval_secs: u64,
pub consecutive_failures: u32,
pub max_failures: u32,
pub total_successes: u64,
pub total_failures: u64,
pub is_failed: bool,
pub failure_action: FailureAction,
}
pub fn validate_config(config: &ReattestConfig) -> Result<()> {
if config.enabled && config.interval_secs == 0 {
return Err(BoxError::Other(
"Re-attestation interval must be > 0 seconds".to_string(),
));
}
if config.enabled && config.max_failures == 0 {
return Err(BoxError::Other(
"Re-attestation max_failures must be > 0".to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reattest_config_default() {
let config = ReattestConfig::default();
assert!(!config.enabled);
assert_eq!(config.interval_secs, 300);
assert_eq!(config.max_failures, 3);
assert_eq!(config.failure_action, FailureAction::Warn);
assert_eq!(config.grace_period_secs, 60);
}
#[test]
fn test_reattest_config_serde_roundtrip() {
let config = ReattestConfig {
enabled: true,
interval_secs: 120,
max_failures: 5,
failure_action: FailureAction::Stop,
grace_period_secs: 30,
};
let json = serde_json::to_string(&config).unwrap();
let parsed: ReattestConfig = serde_json::from_str(&json).unwrap();
assert!(parsed.enabled);
assert_eq!(parsed.interval_secs, 120);
assert_eq!(parsed.max_failures, 5);
assert_eq!(parsed.failure_action, FailureAction::Stop);
assert_eq!(parsed.grace_period_secs, 30);
}
#[test]
fn test_failure_action_default() {
assert_eq!(FailureAction::default(), FailureAction::Warn);
}
#[test]
fn test_reattest_state_disabled() {
let config = ReattestConfig::default(); let state = ReattestState::new(config);
assert!(!state.is_check_due());
}
#[test]
fn test_reattest_state_grace_period() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 3600, ..Default::default()
};
let state = ReattestState::new(config);
assert!(!state.is_check_due());
}
#[test]
fn test_reattest_state_first_check_due() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0, ..Default::default()
};
let state = ReattestState::new(config);
assert!(state.is_check_due());
}
#[test]
fn test_reattest_state_record_success() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0,
..Default::default()
};
let mut state = ReattestState::new(config);
state.record_success();
assert!(state.has_attested());
assert_eq!(state.consecutive_failures(), 0);
assert_eq!(state.total_successes(), 1);
assert_eq!(state.total_failures(), 0);
assert!(!state.is_failed());
}
#[test]
fn test_reattest_state_record_failure_under_threshold() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0,
max_failures: 3,
failure_action: FailureAction::Stop,
..Default::default()
};
let mut state = ReattestState::new(config);
let action = state.record_failure();
assert_eq!(action, FailureAction::Warn);
assert_eq!(state.consecutive_failures(), 1);
assert!(!state.is_failed());
}
#[test]
fn test_reattest_state_record_failure_at_threshold() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0,
max_failures: 2,
failure_action: FailureAction::Stop,
..Default::default()
};
let mut state = ReattestState::new(config);
state.record_failure(); let action = state.record_failure(); assert_eq!(action, FailureAction::Stop);
assert!(state.is_failed());
assert_eq!(state.total_failures(), 2);
}
#[test]
fn test_reattest_state_success_resets_failures() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0,
max_failures: 3,
..Default::default()
};
let mut state = ReattestState::new(config);
state.record_failure();
state.record_failure();
assert_eq!(state.consecutive_failures(), 2);
state.record_success();
assert_eq!(state.consecutive_failures(), 0);
assert!(!state.is_failed());
assert_eq!(state.total_failures(), 2);
assert_eq!(state.total_successes(), 1);
}
#[test]
fn test_reattest_state_not_due_after_recent_check() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0,
interval_secs: 3600, ..Default::default()
};
let mut state = ReattestState::new(config);
assert!(state.is_check_due()); state.record_success();
assert!(!state.is_check_due()); }
#[test]
fn test_reattest_state_summary() {
let config = ReattestConfig {
enabled: true,
interval_secs: 120,
max_failures: 5,
failure_action: FailureAction::Event,
grace_period_secs: 0,
};
let mut state = ReattestState::new(config);
state.record_success();
state.record_failure();
let summary = state.summary();
assert!(summary.enabled);
assert_eq!(summary.interval_secs, 120);
assert_eq!(summary.consecutive_failures, 1);
assert_eq!(summary.max_failures, 5);
assert_eq!(summary.total_successes, 1);
assert_eq!(summary.total_failures, 1);
assert!(!summary.is_failed);
assert_eq!(summary.failure_action, FailureAction::Event);
}
#[test]
fn test_reattest_summary_serde() {
let summary = ReattestSummary {
enabled: true,
interval_secs: 300,
consecutive_failures: 2,
max_failures: 3,
total_successes: 10,
total_failures: 2,
is_failed: false,
failure_action: FailureAction::Warn,
};
let json = serde_json::to_string(&summary).unwrap();
let parsed: ReattestSummary = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.consecutive_failures, 2);
assert_eq!(parsed.total_successes, 10);
}
#[test]
fn test_validate_config_valid() {
let config = ReattestConfig {
enabled: true,
interval_secs: 60,
max_failures: 3,
..Default::default()
};
assert!(validate_config(&config).is_ok());
}
#[test]
fn test_validate_config_disabled_always_valid() {
let config = ReattestConfig {
enabled: false,
interval_secs: 0, max_failures: 0,
..Default::default()
};
assert!(validate_config(&config).is_ok());
}
#[test]
fn test_validate_config_zero_interval() {
let config = ReattestConfig {
enabled: true,
interval_secs: 0,
max_failures: 3,
..Default::default()
};
assert!(validate_config(&config).is_err());
}
#[test]
fn test_validate_config_zero_max_failures() {
let config = ReattestConfig {
enabled: true,
interval_secs: 60,
max_failures: 0,
..Default::default()
};
assert!(validate_config(&config).is_err());
}
#[test]
fn test_time_since_last_success_none() {
let config = ReattestConfig::default();
let state = ReattestState::new(config);
assert!(state.time_since_last_success().is_none());
}
#[test]
fn test_time_since_last_check_none() {
let config = ReattestConfig::default();
let state = ReattestState::new(config);
assert!(state.time_since_last_check().is_none());
}
#[test]
fn test_time_since_last_success_some() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0,
..Default::default()
};
let mut state = ReattestState::new(config);
state.record_success();
let elapsed = state.time_since_last_success().unwrap();
assert!(elapsed < Duration::from_secs(1));
}
#[test]
fn test_failure_action_event() {
let config = ReattestConfig {
enabled: true,
grace_period_secs: 0,
max_failures: 1,
failure_action: FailureAction::Event,
..Default::default()
};
let mut state = ReattestState::new(config);
let action = state.record_failure();
assert_eq!(action, FailureAction::Event);
}
}