1use std::time::{Duration, Instant};
8
9use a3s_box_core::error::{BoxError, Result};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ReattestConfig {
15 pub enabled: bool,
17 #[serde(default = "default_interval")]
19 pub interval_secs: u64,
20 #[serde(default = "default_max_failures")]
22 pub max_failures: u32,
23 #[serde(default)]
25 pub failure_action: FailureAction,
26 #[serde(default = "default_grace_period")]
28 pub grace_period_secs: u64,
29}
30
31fn default_interval() -> u64 {
32 300
33}
34
35fn default_max_failures() -> u32 {
36 3
37}
38
39fn default_grace_period() -> u64 {
40 60
41}
42
43impl Default for ReattestConfig {
44 fn default() -> Self {
45 Self {
46 enabled: false,
47 interval_secs: 300,
48 max_failures: 3,
49 failure_action: FailureAction::default(),
50 grace_period_secs: 60,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub enum FailureAction {
58 #[default]
60 Warn,
61 Event,
63 Stop,
65}
66
67pub struct ReattestState {
69 config: ReattestConfig,
70 boot_time: Instant,
72 last_success: Option<Instant>,
74 last_check: Option<Instant>,
76 consecutive_failures: u32,
78 total_successes: u64,
80 total_failures: u64,
82}
83
84impl ReattestState {
85 pub fn new(config: ReattestConfig) -> Self {
87 Self {
88 config,
89 boot_time: Instant::now(),
90 last_success: None,
91 last_check: None,
92 consecutive_failures: 0,
93 total_successes: 0,
94 total_failures: 0,
95 }
96 }
97
98 pub fn is_check_due(&self) -> bool {
100 if !self.config.enabled {
101 return false;
102 }
103
104 if self.boot_time.elapsed() < Duration::from_secs(self.config.grace_period_secs) {
106 return false;
107 }
108
109 match self.last_check {
110 None => true, Some(last) => last.elapsed() >= Duration::from_secs(self.config.interval_secs),
112 }
113 }
114
115 pub fn record_success(&mut self) {
117 let now = Instant::now();
118 self.last_success = Some(now);
119 self.last_check = Some(now);
120 self.consecutive_failures = 0;
121 self.total_successes += 1;
122 }
123
124 pub fn record_failure(&mut self) -> FailureAction {
126 let now = Instant::now();
127 self.last_check = Some(now);
128 self.consecutive_failures += 1;
129 self.total_failures += 1;
130
131 if self.consecutive_failures >= self.config.max_failures {
132 self.config.failure_action
133 } else {
134 FailureAction::Warn
135 }
136 }
137
138 pub fn consecutive_failures(&self) -> u32 {
140 self.consecutive_failures
141 }
142
143 pub fn total_successes(&self) -> u64 {
145 self.total_successes
146 }
147
148 pub fn total_failures(&self) -> u64 {
150 self.total_failures
151 }
152
153 pub fn time_since_last_success(&self) -> Option<Duration> {
155 self.last_success.map(|t| t.elapsed())
156 }
157
158 pub fn time_since_last_check(&self) -> Option<Duration> {
160 self.last_check.map(|t| t.elapsed())
161 }
162
163 pub fn has_attested(&self) -> bool {
165 self.last_success.is_some()
166 }
167
168 pub fn is_failed(&self) -> bool {
170 self.consecutive_failures >= self.config.max_failures
171 }
172
173 pub fn failure_action(&self) -> FailureAction {
175 self.config.failure_action
176 }
177
178 pub fn summary(&self) -> ReattestSummary {
180 ReattestSummary {
181 enabled: self.config.enabled,
182 interval_secs: self.config.interval_secs,
183 consecutive_failures: self.consecutive_failures,
184 max_failures: self.config.max_failures,
185 total_successes: self.total_successes,
186 total_failures: self.total_failures,
187 is_failed: self.is_failed(),
188 failure_action: self.config.failure_action,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ReattestSummary {
196 pub enabled: bool,
197 pub interval_secs: u64,
198 pub consecutive_failures: u32,
199 pub max_failures: u32,
200 pub total_successes: u64,
201 pub total_failures: u64,
202 pub is_failed: bool,
203 pub failure_action: FailureAction,
204}
205
206pub fn validate_config(config: &ReattestConfig) -> Result<()> {
208 if config.enabled && config.interval_secs == 0 {
209 return Err(BoxError::Other(
210 "Re-attestation interval must be > 0 seconds".to_string(),
211 ));
212 }
213 if config.enabled && config.max_failures == 0 {
214 return Err(BoxError::Other(
215 "Re-attestation max_failures must be > 0".to_string(),
216 ));
217 }
218 Ok(())
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_reattest_config_default() {
227 let config = ReattestConfig::default();
228 assert!(!config.enabled);
229 assert_eq!(config.interval_secs, 300);
230 assert_eq!(config.max_failures, 3);
231 assert_eq!(config.failure_action, FailureAction::Warn);
232 assert_eq!(config.grace_period_secs, 60);
233 }
234
235 #[test]
236 fn test_reattest_config_serde_roundtrip() {
237 let config = ReattestConfig {
238 enabled: true,
239 interval_secs: 120,
240 max_failures: 5,
241 failure_action: FailureAction::Stop,
242 grace_period_secs: 30,
243 };
244 let json = serde_json::to_string(&config).unwrap();
245 let parsed: ReattestConfig = serde_json::from_str(&json).unwrap();
246 assert!(parsed.enabled);
247 assert_eq!(parsed.interval_secs, 120);
248 assert_eq!(parsed.max_failures, 5);
249 assert_eq!(parsed.failure_action, FailureAction::Stop);
250 assert_eq!(parsed.grace_period_secs, 30);
251 }
252
253 #[test]
254 fn test_failure_action_default() {
255 assert_eq!(FailureAction::default(), FailureAction::Warn);
256 }
257
258 #[test]
259 fn test_reattest_state_disabled() {
260 let config = ReattestConfig::default(); let state = ReattestState::new(config);
262 assert!(!state.is_check_due());
263 }
264
265 #[test]
266 fn test_reattest_state_grace_period() {
267 let config = ReattestConfig {
268 enabled: true,
269 grace_period_secs: 3600, ..Default::default()
271 };
272 let state = ReattestState::new(config);
273 assert!(!state.is_check_due());
274 }
275
276 #[test]
277 fn test_reattest_state_first_check_due() {
278 let config = ReattestConfig {
279 enabled: true,
280 grace_period_secs: 0, ..Default::default()
282 };
283 let state = ReattestState::new(config);
284 assert!(state.is_check_due());
285 }
286
287 #[test]
288 fn test_reattest_state_record_success() {
289 let config = ReattestConfig {
290 enabled: true,
291 grace_period_secs: 0,
292 ..Default::default()
293 };
294 let mut state = ReattestState::new(config);
295
296 state.record_success();
297 assert!(state.has_attested());
298 assert_eq!(state.consecutive_failures(), 0);
299 assert_eq!(state.total_successes(), 1);
300 assert_eq!(state.total_failures(), 0);
301 assert!(!state.is_failed());
302 }
303
304 #[test]
305 fn test_reattest_state_record_failure_under_threshold() {
306 let config = ReattestConfig {
307 enabled: true,
308 grace_period_secs: 0,
309 max_failures: 3,
310 failure_action: FailureAction::Stop,
311 ..Default::default()
312 };
313 let mut state = ReattestState::new(config);
314
315 let action = state.record_failure();
317 assert_eq!(action, FailureAction::Warn);
318 assert_eq!(state.consecutive_failures(), 1);
319 assert!(!state.is_failed());
320 }
321
322 #[test]
323 fn test_reattest_state_record_failure_at_threshold() {
324 let config = ReattestConfig {
325 enabled: true,
326 grace_period_secs: 0,
327 max_failures: 2,
328 failure_action: FailureAction::Stop,
329 ..Default::default()
330 };
331 let mut state = ReattestState::new(config);
332
333 state.record_failure(); let action = state.record_failure(); assert_eq!(action, FailureAction::Stop);
336 assert!(state.is_failed());
337 assert_eq!(state.total_failures(), 2);
338 }
339
340 #[test]
341 fn test_reattest_state_success_resets_failures() {
342 let config = ReattestConfig {
343 enabled: true,
344 grace_period_secs: 0,
345 max_failures: 3,
346 ..Default::default()
347 };
348 let mut state = ReattestState::new(config);
349
350 state.record_failure();
351 state.record_failure();
352 assert_eq!(state.consecutive_failures(), 2);
353
354 state.record_success();
355 assert_eq!(state.consecutive_failures(), 0);
356 assert!(!state.is_failed());
357 assert_eq!(state.total_failures(), 2);
359 assert_eq!(state.total_successes(), 1);
360 }
361
362 #[test]
363 fn test_reattest_state_not_due_after_recent_check() {
364 let config = ReattestConfig {
365 enabled: true,
366 grace_period_secs: 0,
367 interval_secs: 3600, ..Default::default()
369 };
370 let mut state = ReattestState::new(config);
371
372 assert!(state.is_check_due()); state.record_success();
374 assert!(!state.is_check_due()); }
376
377 #[test]
378 fn test_reattest_state_summary() {
379 let config = ReattestConfig {
380 enabled: true,
381 interval_secs: 120,
382 max_failures: 5,
383 failure_action: FailureAction::Event,
384 grace_period_secs: 0,
385 };
386 let mut state = ReattestState::new(config);
387 state.record_success();
388 state.record_failure();
389
390 let summary = state.summary();
391 assert!(summary.enabled);
392 assert_eq!(summary.interval_secs, 120);
393 assert_eq!(summary.consecutive_failures, 1);
394 assert_eq!(summary.max_failures, 5);
395 assert_eq!(summary.total_successes, 1);
396 assert_eq!(summary.total_failures, 1);
397 assert!(!summary.is_failed);
398 assert_eq!(summary.failure_action, FailureAction::Event);
399 }
400
401 #[test]
402 fn test_reattest_summary_serde() {
403 let summary = ReattestSummary {
404 enabled: true,
405 interval_secs: 300,
406 consecutive_failures: 2,
407 max_failures: 3,
408 total_successes: 10,
409 total_failures: 2,
410 is_failed: false,
411 failure_action: FailureAction::Warn,
412 };
413 let json = serde_json::to_string(&summary).unwrap();
414 let parsed: ReattestSummary = serde_json::from_str(&json).unwrap();
415 assert_eq!(parsed.consecutive_failures, 2);
416 assert_eq!(parsed.total_successes, 10);
417 }
418
419 #[test]
420 fn test_validate_config_valid() {
421 let config = ReattestConfig {
422 enabled: true,
423 interval_secs: 60,
424 max_failures: 3,
425 ..Default::default()
426 };
427 assert!(validate_config(&config).is_ok());
428 }
429
430 #[test]
431 fn test_validate_config_disabled_always_valid() {
432 let config = ReattestConfig {
433 enabled: false,
434 interval_secs: 0, max_failures: 0,
436 ..Default::default()
437 };
438 assert!(validate_config(&config).is_ok());
439 }
440
441 #[test]
442 fn test_validate_config_zero_interval() {
443 let config = ReattestConfig {
444 enabled: true,
445 interval_secs: 0,
446 max_failures: 3,
447 ..Default::default()
448 };
449 assert!(validate_config(&config).is_err());
450 }
451
452 #[test]
453 fn test_validate_config_zero_max_failures() {
454 let config = ReattestConfig {
455 enabled: true,
456 interval_secs: 60,
457 max_failures: 0,
458 ..Default::default()
459 };
460 assert!(validate_config(&config).is_err());
461 }
462
463 #[test]
464 fn test_time_since_last_success_none() {
465 let config = ReattestConfig::default();
466 let state = ReattestState::new(config);
467 assert!(state.time_since_last_success().is_none());
468 }
469
470 #[test]
471 fn test_time_since_last_check_none() {
472 let config = ReattestConfig::default();
473 let state = ReattestState::new(config);
474 assert!(state.time_since_last_check().is_none());
475 }
476
477 #[test]
478 fn test_time_since_last_success_some() {
479 let config = ReattestConfig {
480 enabled: true,
481 grace_period_secs: 0,
482 ..Default::default()
483 };
484 let mut state = ReattestState::new(config);
485 state.record_success();
486 let elapsed = state.time_since_last_success().unwrap();
487 assert!(elapsed < Duration::from_secs(1));
488 }
489
490 #[test]
491 fn test_failure_action_event() {
492 let config = ReattestConfig {
493 enabled: true,
494 grace_period_secs: 0,
495 max_failures: 1,
496 failure_action: FailureAction::Event,
497 ..Default::default()
498 };
499 let mut state = ReattestState::new(config);
500 let action = state.record_failure();
501 assert_eq!(action, FailureAction::Event);
502 }
503}