use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: i64,
pub timestamp: String,
pub event_type: AuditEventType,
pub subject: String,
pub details: serde_json::Value,
pub operator: String,
pub entry_hash: String,
pub prev_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditEventType {
PolicyEvaluation,
Promotion,
InstallationDetected,
DriftDetected,
ViolationResolved,
OperatorAction,
GateExecution,
ConfigChange,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry() -> AuditEntry {
AuditEntry {
id: 1,
timestamp: "2025-01-01T00:00:00.000Z".to_string(),
event_type: AuditEventType::PolicyEvaluation,
subject: "prod-workspace".to_string(),
details: serde_json::json!({"rule": "require-security-scanner", "passed": false}),
operator: "headless".to_string(),
entry_hash: "abc123".to_string(),
prev_hash: "".to_string(),
}
}
#[test]
fn audit_entry_round_trip() {
let entry = make_entry();
let json = serde_json::to_string(&entry).expect("serialize");
let decoded: AuditEntry = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded.id, 1);
assert_eq!(decoded.timestamp, "2025-01-01T00:00:00.000Z");
assert_eq!(decoded.event_type, AuditEventType::PolicyEvaluation);
assert_eq!(decoded.subject, "prod-workspace");
assert_eq!(decoded.operator, "headless");
assert_eq!(decoded.entry_hash, "abc123");
assert_eq!(decoded.prev_hash, "");
}
#[test]
fn audit_event_type_round_trip() {
let variants = [
AuditEventType::PolicyEvaluation,
AuditEventType::Promotion,
AuditEventType::InstallationDetected,
AuditEventType::DriftDetected,
AuditEventType::ViolationResolved,
AuditEventType::OperatorAction,
AuditEventType::GateExecution,
AuditEventType::ConfigChange,
];
for variant in &variants {
let json = serde_json::to_string(variant).expect("serialize");
let decoded: AuditEventType = serde_json::from_str(&json).expect("deserialize");
assert_eq!(&decoded, variant);
}
}
#[test]
fn audit_event_type_serialized_strings() {
assert_eq!(
serde_json::to_string(&AuditEventType::PolicyEvaluation).unwrap(),
"\"policy_evaluation\""
);
assert_eq!(
serde_json::to_string(&AuditEventType::Promotion).unwrap(),
"\"promotion\""
);
assert_eq!(
serde_json::to_string(&AuditEventType::InstallationDetected).unwrap(),
"\"installation_detected\""
);
assert_eq!(
serde_json::to_string(&AuditEventType::DriftDetected).unwrap(),
"\"drift_detected\""
);
assert_eq!(
serde_json::to_string(&AuditEventType::ViolationResolved).unwrap(),
"\"violation_resolved\""
);
assert_eq!(
serde_json::to_string(&AuditEventType::OperatorAction).unwrap(),
"\"operator_action\""
);
assert_eq!(
serde_json::to_string(&AuditEventType::GateExecution).unwrap(),
"\"gate_execution\""
);
assert_eq!(
serde_json::to_string(&AuditEventType::ConfigChange).unwrap(),
"\"config_change\""
);
}
#[test]
fn audit_entry_details_is_arbitrary_json() {
let entry = AuditEntry {
id: 42,
timestamp: "2025-06-01T12:00:00.000Z".to_string(),
event_type: AuditEventType::GateExecution,
subject: "validate:lint".to_string(),
details: serde_json::json!({
"gate": "validate:lint",
"exit_code": 0,
"duration_ms": 1500,
}),
operator: "system".to_string(),
entry_hash: "deadbeef".to_string(),
prev_hash: "cafebabe".to_string(),
};
let json = serde_json::to_string(&entry).unwrap();
let decoded: AuditEntry = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.details["gate"], "validate:lint");
assert_eq!(decoded.details["exit_code"], 0);
}
}