use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Default)]
pub struct StateSnapshotParams {}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct StateSnapshot {
pub pc_id: String,
pub online: bool,
pub vpn: String,
pub checks: Vec<Check>,
pub agent_version: String,
pub target_version: String,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct Check {
pub name: String,
pub status: CheckStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub troubleshoot: Option<String>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum CheckStatus {
Ok,
Warn,
Fail,
Unknown,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Default)]
pub struct StateSubscribeParams {}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct StateSubscribeResult {
pub subscription: String,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct StateUnsubscribeParams {
pub subscription: String,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct StateChangedParams {
pub snapshot: StateSnapshot,
pub at: chrono::DateTime<chrono::Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_status_serialises_snake_case() {
for (variant, expected) in [
(CheckStatus::Ok, "\"ok\""),
(CheckStatus::Warn, "\"warn\""),
(CheckStatus::Fail, "\"fail\""),
(CheckStatus::Unknown, "\"unknown\""),
] {
let s = serde_json::to_string(&variant).unwrap();
assert_eq!(s, expected, "encode {variant:?}");
let back: CheckStatus = serde_json::from_str(expected).unwrap();
assert_eq!(back, variant, "round-trip {expected}");
}
}
#[test]
fn state_snapshot_spec_example_decodes() {
let wire = r#"{
"pc_id":"PC1234","online":true,"vpn":"connected",
"checks":[{"name":"bitlocker","status":"ok"},
{"name":"av_signature","status":"warn","detail":"3 日前"}],
"agent_version":"0.4.0","target_version":"0.4.0"
}"#;
let s: StateSnapshot = serde_json::from_str(wire).expect("decode");
assert_eq!(s.pc_id, "PC1234");
assert!(s.online);
assert_eq!(s.vpn, "connected");
assert_eq!(s.checks.len(), 2);
assert_eq!(s.checks[0].name, "bitlocker");
assert_eq!(s.checks[0].status, CheckStatus::Ok);
assert_eq!(s.checks[1].name, "av_signature");
assert_eq!(s.checks[1].status, CheckStatus::Warn);
assert_eq!(s.checks[1].detail.as_deref(), Some("3 日前"));
assert_eq!(s.agent_version, "0.4.0");
assert_eq!(s.target_version, "0.4.0");
}
#[test]
fn check_with_troubleshoot_round_trips() {
let c = Check {
name: "av_signature".into(),
status: CheckStatus::Fail,
detail: Some("Signatures > 7 days old".into()),
troubleshoot: Some("update-av-signatures".into()),
};
let json = serde_json::to_string(&c).unwrap();
let back: Check = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, c.name);
assert_eq!(back.status, c.status);
assert_eq!(back.detail, c.detail);
assert_eq!(back.troubleshoot, c.troubleshoot);
}
#[test]
fn check_without_optional_fields_decodes() {
let c = Check {
name: "bitlocker".into(),
status: CheckStatus::Ok,
detail: None,
troubleshoot: None,
};
let v = serde_json::to_value(&c).unwrap();
assert!(v.get("detail").is_none(), "wire: {v:?}");
assert!(v.get("troubleshoot").is_none(), "wire: {v:?}");
}
#[test]
fn state_changed_push_round_trips() {
let p = StateChangedParams {
snapshot: StateSnapshot {
pc_id: "PC1234".into(),
online: true,
vpn: "connected".into(),
checks: vec![],
agent_version: "0.4.0".into(),
target_version: "0.4.0".into(),
},
at: chrono::Utc::now(),
};
let json = serde_json::to_string(&p).unwrap();
let back: StateChangedParams = serde_json::from_str(&json).unwrap();
assert_eq!(back.snapshot.pc_id, "PC1234");
}
}