Skip to main content

fraiseql_server/encryption/
dashboard.rs

1// Phase 12.4 Cycle 4: Dashboard & Monitoring - GREEN
2//! Rotation dashboard, metrics visualization, compliance monitoring,
3//! and alert/notification systems for rotation management.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8/// Dashboard overview with all keys status
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DashboardOverview {
11    /// Total number of encryption keys
12    pub total_keys:      usize,
13    /// Keys with <70% TTL consumed
14    pub healthy_keys:    usize,
15    /// Keys with 70-85% TTL consumed
16    pub warning_keys:    usize,
17    /// Keys with 85%+ TTL consumed
18    pub urgent_keys:     usize,
19    /// Average TTL consumption percentage across all keys
20    pub avg_ttl_percent: u32,
21    /// Overall system health status
22    pub system_health:   String,
23}
24
25impl DashboardOverview {
26    /// Create new dashboard overview
27    pub fn new() -> Self {
28        Self {
29            total_keys:      0,
30            healthy_keys:    0,
31            warning_keys:    0,
32            urgent_keys:     0,
33            avg_ttl_percent: 0,
34            system_health:   "healthy".to_string(),
35        }
36    }
37
38    /// Calculate system health status
39    pub fn recalculate_health(&mut self) {
40        if self.urgent_keys > 0 {
41            self.system_health = "critical".to_string();
42        } else if self.warning_keys > 0 {
43            self.system_health = "warning".to_string();
44        } else {
45            self.system_health = "healthy".to_string();
46        }
47    }
48}
49
50impl Default for DashboardOverview {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56/// Key status card for dashboard display
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct KeyStatusCard {
59    /// Key identifier
60    pub key_id:             String,
61    /// Current active version
62    pub current_version:    u16,
63    /// TTL consumption percentage (0-100)
64    pub ttl_percent:        u32,
65    /// Status level: "healthy", "warning", "urgent", "overdue"
66    pub status:             String,
67    /// Last rotation timestamp
68    pub last_rotation:      Option<DateTime<Utc>>,
69    /// Estimated next rotation time
70    pub next_rotation:      Option<DateTime<Utc>>,
71    /// Total versions count
72    pub versions_count:     usize,
73    /// Urgency score (0-100)
74    pub urgency_score:      u32,
75    /// Recommended action
76    pub recommended_action: String,
77}
78
79impl KeyStatusCard {
80    /// Create new key status card
81    pub fn new(key_id: impl Into<String>, current_version: u16, ttl_percent: u32) -> Self {
82        let (status, urgency_score, recommended_action) = match ttl_percent {
83            0..=40 => ("healthy".to_string(), 10, "Monitor key health".to_string()),
84            41..=70 => ("healthy".to_string(), 30, "Monitor key health".to_string()),
85            71..=85 => ("warning".to_string(), 60, "Prepare for upcoming rotation".to_string()),
86            86..=99 => ("urgent".to_string(), 85, "Trigger manual rotation".to_string()),
87            _ => ("overdue".to_string(), 100, "CRITICAL: Rotate immediately".to_string()),
88        };
89
90        Self {
91            key_id: key_id.into(),
92            current_version,
93            ttl_percent,
94            status,
95            last_rotation: None,
96            next_rotation: None,
97            versions_count: 1,
98            urgency_score,
99            recommended_action,
100        }
101    }
102}
103
104/// Rotation metrics for time series visualization
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct RotationMetricsPoint {
107    /// Timestamp for this data point
108    pub timestamp:                DateTime<Utc>,
109    /// Total rotations count
110    pub rotations_total:          u64,
111    /// Manual rotations count
112    pub rotations_manual:         u64,
113    /// Auto-refresh rotations count
114    pub rotations_auto:           u64,
115    /// Average rotation duration in milliseconds
116    pub rotation_duration_avg_ms: u64,
117    /// Rotation success rate percentage
118    pub success_rate_percent:     u32,
119}
120
121impl RotationMetricsPoint {
122    /// Create new metrics point
123    pub fn new(timestamp: DateTime<Utc>) -> Self {
124        Self {
125            timestamp,
126            rotations_total: 0,
127            rotations_manual: 0,
128            rotations_auto: 0,
129            rotation_duration_avg_ms: 0,
130            success_rate_percent: 100,
131        }
132    }
133}
134
135/// Time series data for metrics charts
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct MetricsTimeSeries {
138    /// Period: "1d", "7d", "30d", "90d"
139    pub period:      String,
140    /// Data points
141    pub data_points: Vec<RotationMetricsPoint>,
142}
143
144impl MetricsTimeSeries {
145    /// Create new time series
146    pub fn new(period: impl Into<String>) -> Self {
147        Self {
148            period:      period.into(),
149            data_points: Vec::new(),
150        }
151    }
152}
153
154/// Compliance framework status
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
156pub enum ComplianceStatus {
157    /// All requirements met
158    Compliant,
159    /// Some requirements not met
160    Partial,
161    /// Requirements not met
162    NonCompliant,
163}
164
165impl std::fmt::Display for ComplianceStatus {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        match self {
168            Self::Compliant => write!(f, "compliant"),
169            Self::Partial => write!(f, "partial"),
170            Self::NonCompliant => write!(f, "non_compliant"),
171        }
172    }
173}
174
175/// Compliance requirement check
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ComplianceRequirement {
178    /// Requirement name
179    pub name:    String,
180    /// Is requirement met
181    pub met:     bool,
182    /// Details about requirement
183    pub details: String,
184}
185
186/// Compliance framework dashboard
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct ComplianceDashboard {
189    /// HIPAA compliance status
190    pub hipaa:   ComplianceStatus,
191    /// PCI-DSS compliance status
192    pub pci_dss: ComplianceStatus,
193    /// GDPR compliance status
194    pub gdpr:    ComplianceStatus,
195    /// SOC 2 compliance status
196    pub soc2:    ComplianceStatus,
197    /// Overall compliance status
198    pub overall: ComplianceStatus,
199}
200
201impl ComplianceDashboard {
202    /// Create new compliance dashboard
203    pub fn new() -> Self {
204        Self {
205            hipaa:   ComplianceStatus::Compliant,
206            pci_dss: ComplianceStatus::Compliant,
207            gdpr:    ComplianceStatus::Compliant,
208            soc2:    ComplianceStatus::Compliant,
209            overall: ComplianceStatus::Compliant,
210        }
211    }
212
213    /// Recalculate overall compliance
214    pub fn recalculate_overall(&mut self) {
215        // If any is non-compliant, overall is non-compliant
216        if matches!(self.hipaa, ComplianceStatus::NonCompliant)
217            || matches!(self.pci_dss, ComplianceStatus::NonCompliant)
218            || matches!(self.gdpr, ComplianceStatus::NonCompliant)
219            || matches!(self.soc2, ComplianceStatus::NonCompliant)
220        {
221            self.overall = ComplianceStatus::NonCompliant;
222        } else if matches!(self.hipaa, ComplianceStatus::Partial)
223            || matches!(self.pci_dss, ComplianceStatus::Partial)
224            || matches!(self.gdpr, ComplianceStatus::Partial)
225            || matches!(self.soc2, ComplianceStatus::Partial)
226        {
227            self.overall = ComplianceStatus::Partial;
228        } else {
229            self.overall = ComplianceStatus::Compliant;
230        }
231    }
232}
233
234impl Default for ComplianceDashboard {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240/// Alert severity level
241#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
242#[serde(rename_all = "lowercase")]
243pub enum AlertSeverity {
244    /// Informational
245    Info,
246    /// Warning
247    Warning,
248    /// Error
249    Error,
250    /// Critical
251    Critical,
252}
253
254impl std::fmt::Display for AlertSeverity {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            Self::Info => write!(f, "info"),
258            Self::Warning => write!(f, "warning"),
259            Self::Error => write!(f, "error"),
260            Self::Critical => write!(f, "critical"),
261        }
262    }
263}
264
265/// Alert notification
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct Alert {
268    /// Unique alert ID
269    pub id:           String,
270    /// Alert type
271    pub alert_type:   String,
272    /// Severity level
273    pub severity:     AlertSeverity,
274    /// Affected key ID
275    pub key_id:       Option<String>,
276    /// Alert timestamp
277    pub timestamp:    DateTime<Utc>,
278    /// Alert message
279    pub message:      String,
280    /// Recommended action
281    pub action:       Option<String>,
282    /// Is alert read/acknowledged
283    pub acknowledged: bool,
284}
285
286impl Alert {
287    /// Create new alert
288    pub fn new(
289        alert_type: impl Into<String>,
290        severity: AlertSeverity,
291        message: impl Into<String>,
292    ) -> Self {
293        Self {
294            id: uuid::Uuid::new_v4().to_string(),
295            alert_type: alert_type.into(),
296            severity,
297            key_id: None,
298            timestamp: Utc::now(),
299            message: message.into(),
300            action: None,
301            acknowledged: false,
302        }
303    }
304
305    /// Set affected key
306    pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
307        self.key_id = Some(key_id.into());
308        self
309    }
310
311    /// Set recommended action
312    pub fn with_action(mut self, action: impl Into<String>) -> Self {
313        self.action = Some(action.into());
314        self
315    }
316
317    /// Mark alert as acknowledged
318    pub fn acknowledge(&mut self) {
319        self.acknowledged = true;
320    }
321}
322
323/// Dashboard alerts widget
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct AlertsWidget {
326    /// Active (unacknowledged) alerts
327    pub active_alerts:        Vec<Alert>,
328    /// Historical alerts
329    pub historical_alerts:    Vec<Alert>,
330    /// Total unacknowledged count
331    pub unacknowledged_count: usize,
332}
333
334impl AlertsWidget {
335    /// Create new alerts widget
336    pub fn new() -> Self {
337        Self {
338            active_alerts:        Vec::new(),
339            historical_alerts:    Vec::new(),
340            unacknowledged_count: 0,
341        }
342    }
343
344    /// Add alert
345    pub fn add_alert(&mut self, alert: Alert) {
346        if !alert.acknowledged {
347            self.unacknowledged_count += 1;
348        }
349        self.active_alerts.push(alert);
350    }
351
352    /// Clear old alerts (older than N days)
353    pub fn clear_old_alerts(&mut self, days: i64) {
354        let cutoff = Utc::now() - chrono::Duration::days(days);
355        self.active_alerts.retain(|a| a.timestamp > cutoff || !a.acknowledged);
356    }
357}
358
359impl Default for AlertsWidget {
360    fn default() -> Self {
361        Self::new()
362    }
363}
364
365/// Trend analysis result
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct TrendAnalysis {
368    /// Rotation frequency trend: "increasing", "stable", "decreasing"
369    pub rotation_frequency: String,
370    /// Duration trend: "increasing", "stable", "decreasing"
371    pub rotation_duration:  String,
372    /// Failure rate trend: "increasing", "stable", "decreasing"
373    pub failure_rate:       String,
374    /// Compliance trend: "improving", "stable", "degrading"
375    pub compliance_status:  String,
376}
377
378impl TrendAnalysis {
379    /// Create new trend analysis
380    pub fn new() -> Self {
381        Self {
382            rotation_frequency: "stable".to_string(),
383            rotation_duration:  "stable".to_string(),
384            failure_rate:       "stable".to_string(),
385            compliance_status:  "stable".to_string(),
386        }
387    }
388}
389
390impl Default for TrendAnalysis {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396/// Dashboard export format options
397#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
398pub enum ExportFormat {
399    /// JSON format
400    Json,
401    /// CSV format
402    Csv,
403    /// PDF report format
404    Pdf,
405}
406
407impl std::fmt::Display for ExportFormat {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        match self {
410            Self::Json => write!(f, "json"),
411            Self::Csv => write!(f, "csv"),
412            Self::Pdf => write!(f, "pdf"),
413        }
414    }
415}
416
417/// Dashboard snapshot for export
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct DashboardSnapshot {
420    /// Snapshot timestamp
421    pub snapshot_time:       DateTime<Utc>,
422    /// Dashboard overview at this time
423    pub overview:            DashboardOverview,
424    /// Key status cards
425    pub key_cards:           Vec<KeyStatusCard>,
426    /// Alert summary
427    pub active_alerts_count: usize,
428    /// Compliance status
429    pub compliance:          ComplianceDashboard,
430}
431
432impl DashboardSnapshot {
433    /// Create new dashboard snapshot
434    pub fn new(
435        overview: DashboardOverview,
436        key_cards: Vec<KeyStatusCard>,
437        compliance: ComplianceDashboard,
438        active_alerts_count: usize,
439    ) -> Self {
440        Self {
441            snapshot_time: Utc::now(),
442            overview,
443            key_cards,
444            active_alerts_count,
445            compliance,
446        }
447    }
448
449    /// Get urgency summary (count of keys by status)
450    pub fn urgency_summary(&self) -> (usize, usize, usize, usize) {
451        let mut healthy = 0;
452        let mut warning = 0;
453        let mut urgent = 0;
454        let mut overdue = 0;
455
456        for card in &self.key_cards {
457            match card.status.as_str() {
458                "healthy" => healthy += 1,
459                "warning" => warning += 1,
460                "urgent" => urgent += 1,
461                "overdue" => overdue += 1,
462                _ => {},
463            }
464        }
465
466        (healthy, warning, urgent, overdue)
467    }
468
469    /// Calculate average urgency score across all keys
470    pub fn average_urgency_score(&self) -> u32 {
471        if self.key_cards.is_empty() {
472            return 0;
473        }
474
475        let total: u32 = self.key_cards.iter().map(|c| c.urgency_score).sum();
476        total / self.key_cards.len() as u32
477    }
478}
479
480/// Alert filter for querying and filtering alerts
481#[derive(Debug, Clone)]
482pub struct AlertFilter {
483    /// Filter by severity
484    pub severity:     Option<AlertSeverity>,
485    /// Filter by alert type
486    pub alert_type:   Option<String>,
487    /// Filter by key ID
488    pub key_id:       Option<String>,
489    /// Filter acknowledged status
490    pub acknowledged: Option<bool>,
491}
492
493impl AlertFilter {
494    /// Create new alert filter
495    pub fn new() -> Self {
496        Self {
497            severity:     None,
498            alert_type:   None,
499            key_id:       None,
500            acknowledged: None,
501        }
502    }
503
504    /// Filter alerts based on criteria
505    pub fn apply(&self, alerts: &[Alert]) -> Vec<Alert> {
506        alerts
507            .iter()
508            .filter(|alert| {
509                if let Some(sev) = self.severity {
510                    if alert.severity != sev {
511                        return false;
512                    }
513                }
514                if let Some(ref alert_type) = self.alert_type {
515                    if alert.alert_type != *alert_type {
516                        return false;
517                    }
518                }
519                if let Some(ref key_id) = self.key_id {
520                    if alert.key_id.as_ref() != Some(key_id) {
521                        return false;
522                    }
523                }
524                if let Some(ack) = self.acknowledged {
525                    if alert.acknowledged != ack {
526                        return false;
527                    }
528                }
529                true
530            })
531            .cloned()
532            .collect()
533    }
534}
535
536impl Default for AlertFilter {
537    fn default() -> Self {
538        Self::new()
539    }
540}
541
542/// Compliance requirement checker
543#[derive(Debug, Clone)]
544pub struct ComplianceChecker {
545    /// Framework name
546    pub framework:                     String,
547    /// Required TTL in days
548    pub required_ttl_days:             u32,
549    /// Required check interval in hours
550    pub required_check_interval_hours: u32,
551}
552
553impl ComplianceChecker {
554    /// Create checker for HIPAA compliance
555    pub fn hipaa() -> Self {
556        Self {
557            framework:                     "HIPAA".to_string(),
558            required_ttl_days:             365,
559            required_check_interval_hours: 24,
560        }
561    }
562
563    /// Create checker for PCI-DSS compliance
564    pub fn pci_dss() -> Self {
565        Self {
566            framework:                     "PCI-DSS".to_string(),
567            required_ttl_days:             365,
568            required_check_interval_hours: 24,
569        }
570    }
571
572    /// Create checker for GDPR compliance
573    pub fn gdpr() -> Self {
574        Self {
575            framework:                     "GDPR".to_string(),
576            required_ttl_days:             90,
577            required_check_interval_hours: 24,
578        }
579    }
580
581    /// Create checker for SOC 2 compliance
582    pub fn soc2() -> Self {
583        Self {
584            framework:                     "SOC 2".to_string(),
585            required_ttl_days:             365,
586            required_check_interval_hours: 24,
587        }
588    }
589
590    /// Check if card meets compliance requirements
591    pub fn check_compliance(&self, card: &KeyStatusCard) -> bool {
592        // For compliance, key should not be urgent or overdue
593        !matches!(card.status.as_str(), "urgent" | "overdue")
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600
601    #[test]
602    fn test_dashboard_overview_creation() {
603        let overview = DashboardOverview::new();
604        assert_eq!(overview.total_keys, 0);
605        assert_eq!(overview.system_health, "healthy");
606    }
607
608    #[test]
609    fn test_dashboard_overview_health_critical() {
610        let mut overview = DashboardOverview {
611            total_keys:      1,
612            healthy_keys:    0,
613            warning_keys:    0,
614            urgent_keys:     1,
615            avg_ttl_percent: 95,
616            system_health:   "healthy".to_string(),
617        };
618        overview.recalculate_health();
619        assert_eq!(overview.system_health, "critical");
620    }
621
622    #[test]
623    fn test_key_status_card_healthy() {
624        let card = KeyStatusCard::new("primary", 1, 50);
625        assert_eq!(card.status, "healthy");
626        assert_eq!(card.urgency_score, 30);
627    }
628
629    #[test]
630    fn test_key_status_card_urgent() {
631        let card = KeyStatusCard::new("primary", 1, 90);
632        assert_eq!(card.status, "urgent");
633        assert_eq!(card.urgency_score, 85);
634    }
635
636    #[test]
637    fn test_key_status_card_overdue() {
638        let card = KeyStatusCard::new("primary", 1, 105);
639        assert_eq!(card.status, "overdue");
640        assert_eq!(card.urgency_score, 100);
641    }
642
643    #[test]
644    fn test_rotation_metrics_point_creation() {
645        let now = Utc::now();
646        let point = RotationMetricsPoint::new(now);
647        assert_eq!(point.timestamp, now);
648        assert_eq!(point.rotations_total, 0);
649        assert_eq!(point.success_rate_percent, 100);
650    }
651
652    #[test]
653    fn test_metrics_time_series_creation() {
654        let series = MetricsTimeSeries::new("30d");
655        assert_eq!(series.period, "30d");
656        assert_eq!(series.data_points.len(), 0);
657    }
658
659    #[test]
660    fn test_compliance_dashboard_creation() {
661        let dashboard = ComplianceDashboard::new();
662        assert_eq!(dashboard.overall, ComplianceStatus::Compliant);
663    }
664
665    #[test]
666    fn test_compliance_dashboard_recalculate_non_compliant() {
667        let mut dashboard = ComplianceDashboard::new();
668        dashboard.hipaa = ComplianceStatus::NonCompliant;
669        dashboard.recalculate_overall();
670        assert_eq!(dashboard.overall, ComplianceStatus::NonCompliant);
671    }
672
673    #[test]
674    fn test_compliance_dashboard_recalculate_partial() {
675        let mut dashboard = ComplianceDashboard::new();
676        dashboard.gdpr = ComplianceStatus::Partial;
677        dashboard.recalculate_overall();
678        assert_eq!(dashboard.overall, ComplianceStatus::Partial);
679    }
680
681    #[test]
682    fn test_alert_creation() {
683        let alert = Alert::new("rotation_failed", AlertSeverity::Critical, "Rotation failed");
684        assert_eq!(alert.alert_type, "rotation_failed");
685        assert_eq!(alert.severity, AlertSeverity::Critical);
686        assert!(!alert.acknowledged);
687    }
688
689    #[test]
690    fn test_alert_with_key_id() {
691        let alert = Alert::new("rotation_failed", AlertSeverity::Error, "Rotation failed")
692            .with_key_id("primary");
693        assert_eq!(alert.key_id, Some("primary".to_string()));
694    }
695
696    #[test]
697    fn test_alert_acknowledge() {
698        let mut alert = Alert::new("rotation_failed", AlertSeverity::Error, "Rotation failed");
699        assert!(!alert.acknowledged);
700        alert.acknowledge();
701        assert!(alert.acknowledged);
702    }
703
704    #[test]
705    fn test_alerts_widget_creation() {
706        let widget = AlertsWidget::new();
707        assert_eq!(widget.active_alerts.len(), 0);
708        assert_eq!(widget.unacknowledged_count, 0);
709    }
710
711    #[test]
712    fn test_alerts_widget_add_alert() {
713        let mut widget = AlertsWidget::new();
714        let alert = Alert::new("rotation_failed", AlertSeverity::Error, "Failed");
715        widget.add_alert(alert);
716        assert_eq!(widget.active_alerts.len(), 1);
717        assert_eq!(widget.unacknowledged_count, 1);
718    }
719
720    #[test]
721    fn test_trend_analysis_creation() {
722        let trend = TrendAnalysis::new();
723        assert_eq!(trend.rotation_frequency, "stable");
724        assert_eq!(trend.compliance_status, "stable");
725    }
726
727    #[test]
728    fn test_dashboard_snapshot_creation() {
729        let overview = DashboardOverview::new();
730        let key_cards = vec![
731            KeyStatusCard::new("primary", 1, 50),
732            KeyStatusCard::new("secondary", 1, 80),
733        ];
734        let compliance = ComplianceDashboard::new();
735        let snapshot = DashboardSnapshot::new(overview, key_cards, compliance, 0);
736        assert_eq!(snapshot.key_cards.len(), 2);
737    }
738
739    #[test]
740    fn test_dashboard_snapshot_urgency_summary() {
741        let overview = DashboardOverview::new();
742        let key_cards = vec![
743            KeyStatusCard::new("primary", 1, 30),    // healthy
744            KeyStatusCard::new("secondary", 1, 50),  // healthy
745            KeyStatusCard::new("tertiary", 1, 75),   // warning
746            KeyStatusCard::new("quaternary", 1, 90), // urgent
747        ];
748        let compliance = ComplianceDashboard::new();
749        let snapshot = DashboardSnapshot::new(overview, key_cards, compliance, 0);
750        let (healthy, warning, urgent, overdue) = snapshot.urgency_summary();
751        assert_eq!(healthy, 2);
752        assert_eq!(warning, 1);
753        assert_eq!(urgent, 1);
754        assert_eq!(overdue, 0);
755    }
756
757    #[test]
758    fn test_dashboard_snapshot_average_urgency_score() {
759        let overview = DashboardOverview::new();
760        let key_cards = vec![
761            KeyStatusCard::new("primary", 1, 50),   // urgency_score: 30
762            KeyStatusCard::new("secondary", 1, 80), // urgency_score: 60
763        ];
764        let compliance = ComplianceDashboard::new();
765        let snapshot = DashboardSnapshot::new(overview, key_cards, compliance, 0);
766        assert_eq!(snapshot.average_urgency_score(), 45);
767    }
768
769    #[test]
770    fn test_alert_filter_by_severity() {
771        let mut filter = AlertFilter::new();
772        filter.severity = Some(AlertSeverity::Critical);
773
774        let alerts = vec![
775            Alert::new("rotation_failed", AlertSeverity::Critical, "Failed"),
776            Alert::new("rotation_warning", AlertSeverity::Warning, "Warning"),
777        ];
778
779        let filtered = filter.apply(&alerts);
780        assert_eq!(filtered.len(), 1);
781        assert_eq!(filtered[0].severity, AlertSeverity::Critical);
782    }
783
784    #[test]
785    fn test_alert_filter_by_acknowledged() {
786        let mut filter = AlertFilter::new();
787        filter.acknowledged = Some(false);
788
789        let alert1 = Alert::new("rotation_failed", AlertSeverity::Error, "Failed");
790        let mut alert2 = Alert::new("rotation_warning", AlertSeverity::Warning, "Warning");
791        alert2.acknowledge();
792
793        let alerts = vec![alert1, alert2];
794        let filtered = filter.apply(&alerts);
795        assert_eq!(filtered.len(), 1);
796        assert!(!filtered[0].acknowledged);
797    }
798
799    #[test]
800    fn test_compliance_checker_hipaa() {
801        let checker = ComplianceChecker::hipaa();
802        assert_eq!(checker.framework, "HIPAA");
803        assert_eq!(checker.required_ttl_days, 365);
804    }
805
806    #[test]
807    fn test_compliance_checker_gdpr() {
808        let checker = ComplianceChecker::gdpr();
809        assert_eq!(checker.framework, "GDPR");
810        assert_eq!(checker.required_ttl_days, 90);
811    }
812
813    #[test]
814    fn test_compliance_checker_validates_key_status() {
815        let checker = ComplianceChecker::hipaa();
816        let healthy_card = KeyStatusCard::new("primary", 1, 50);
817        let urgent_card = KeyStatusCard::new("primary", 1, 90);
818
819        assert!(checker.check_compliance(&healthy_card));
820        assert!(!checker.check_compliance(&urgent_card));
821    }
822
823    #[test]
824    fn test_export_format_display() {
825        assert_eq!(ExportFormat::Json.to_string(), "json");
826        assert_eq!(ExportFormat::Csv.to_string(), "csv");
827        assert_eq!(ExportFormat::Pdf.to_string(), "pdf");
828    }
829}