use std::fs;
use std::path::PathBuf;
use synapse_pingora::horizon::{
AuthPayload, CommandAckPayload, HeartbeatPayload, HubMessage, SensorMessage, Severity,
SignalType, ThreatSignal,
};
fn fixtures_dir() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.join("..")
.join("..")
.join("packages")
.join("synapse-api")
.join("fixtures")
}
fn read_fixture(name: &str) -> String {
let path = fixtures_dir().join(format!("{}.json", name));
fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"Failed to read fixture '{}' at {}: {}. \
Run `cd packages/synapse-api && npx vitest run` first to generate fixtures.",
name,
path.display(),
e
)
})
}
#[test]
fn protocol_compat_sensor_auth() {
let json = read_fixture("sensor-auth");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-auth");
match &msg {
SensorMessage::Auth { payload } => {
assert_eq!(payload.sensor_id, "sensor-prod-01");
assert_eq!(payload.version, "1.2.0");
assert_eq!(payload.protocol_version, Some("1.0".to_string()));
assert!(payload.sensor_name.is_some());
}
other => panic!("Expected Auth, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_auth_minimal() {
let json = read_fixture("sensor-auth-minimal");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-auth-minimal");
match &msg {
SensorMessage::Auth { payload } => {
assert_eq!(payload.sensor_id, "sensor-dev-01");
assert!(payload.sensor_name.is_none());
assert!(payload.protocol_version.is_none());
}
other => panic!("Expected Auth, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_signal() {
let json = read_fixture("sensor-signal");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-signal");
match &msg {
SensorMessage::Signal { payload } => {
assert_eq!(payload.signal_type, SignalType::IpThreat);
assert_eq!(payload.severity, Severity::High);
assert_eq!(payload.confidence, 0.95);
assert_eq!(payload.source_ip, Some("192.168.1.100".to_string()));
assert_eq!(payload.event_count, Some(50));
assert!(payload.metadata.is_some());
}
other => panic!("Expected Signal, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_signal_minimal() {
let json = read_fixture("sensor-signal-minimal");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-signal-minimal");
match &msg {
SensorMessage::Signal { payload } => {
assert_eq!(payload.signal_type, SignalType::BotSignature);
assert_eq!(payload.severity, Severity::Medium);
assert!(payload.source_ip.is_none());
assert!(payload.event_count.is_none());
assert!(payload.metadata.is_none());
}
other => panic!("Expected Signal, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_signal_batch() {
let json = read_fixture("sensor-signal-batch");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-signal-batch");
match &msg {
SensorMessage::SignalBatch { payload } => {
assert_eq!(payload.len(), 3);
assert_eq!(payload[0].signal_type, SignalType::CredentialStuffing);
assert_eq!(payload[0].severity, Severity::Critical);
assert_eq!(payload[1].signal_type, SignalType::RateAnomaly);
assert_eq!(payload[2].signal_type, SignalType::FingerprintThreat);
}
other => panic!("Expected SignalBatch, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_pong() {
let json = read_fixture("sensor-pong");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-pong");
assert!(matches!(msg, SensorMessage::Pong));
}
#[test]
fn protocol_compat_sensor_blocklist_sync() {
let json = read_fixture("sensor-blocklist-sync");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-blocklist-sync");
assert!(matches!(msg, SensorMessage::BlocklistSync));
}
#[test]
fn protocol_compat_sensor_heartbeat() {
let json = read_fixture("sensor-heartbeat");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-heartbeat");
match &msg {
SensorMessage::Heartbeat { payload } => {
assert_eq!(payload.timestamp, 1706745600000);
assert_eq!(payload.status, "healthy");
assert_eq!(payload.cpu, 45.2);
assert_eq!(payload.memory, 62.1);
assert_eq!(payload.requests_last_minute, 15420);
assert_eq!(payload.active_connections, Some(342));
assert_eq!(payload.blocklist_size, Some(1500));
}
other => panic!("Expected Heartbeat, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_heartbeat_minimal() {
let json = read_fixture("sensor-heartbeat-minimal");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-heartbeat-minimal");
match &msg {
SensorMessage::Heartbeat { payload } => {
assert_eq!(payload.status, "degraded");
assert!(payload.active_connections.is_none());
assert!(payload.blocklist_size.is_none());
}
other => panic!("Expected Heartbeat, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_command_ack() {
let json = read_fixture("sensor-command-ack");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-command-ack");
match &msg {
SensorMessage::CommandAck { payload } => {
assert_eq!(payload.command_id, "cmd-abc-123");
assert!(payload.success);
assert!(payload.message.is_some());
assert!(payload.result.is_some());
}
other => panic!("Expected CommandAck, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_command_ack_failed() {
let json = read_fixture("sensor-command-ack-failed");
let msg: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize sensor-command-ack-failed");
match &msg {
SensorMessage::CommandAck { payload } => {
assert!(!payload.success);
assert!(payload.result.is_none());
}
other => panic!("Expected CommandAck, got {:?}", other),
}
}
#[test]
fn protocol_compat_sensor_all_signal_types() {
let json = read_fixture("sensor-signal-all-types");
let messages: Vec<SensorMessage> =
serde_json::from_str(&json).expect("Failed to deserialize sensor-signal-all-types");
assert_eq!(messages.len(), 9);
let expected_types = vec![
SignalType::IpThreat,
SignalType::FingerprintThreat,
SignalType::CampaignIndicator,
SignalType::CredentialStuffing,
SignalType::RateAnomaly,
SignalType::BotSignature,
SignalType::ImpossibleTravel,
SignalType::TemplateDiscovery,
SignalType::SchemaViolation,
];
for (msg, expected) in messages.iter().zip(expected_types.iter()) {
match msg {
SensorMessage::Signal { payload } => {
assert_eq!(&payload.signal_type, expected);
}
other => panic!("Expected Signal, got {:?}", other),
}
}
}
#[test]
fn protocol_compat_sensor_all_severities() {
let json = read_fixture("sensor-signal-all-severities");
let messages: Vec<SensorMessage> =
serde_json::from_str(&json).expect("Failed to deserialize sensor-signal-all-severities");
assert_eq!(messages.len(), 4);
let expected_severities = vec![
Severity::Low,
Severity::Medium,
Severity::High,
Severity::Critical,
];
for (msg, expected) in messages.iter().zip(expected_severities.iter()) {
match msg {
SensorMessage::Signal { payload } => {
assert_eq!(&payload.severity, expected);
}
other => panic!("Expected Signal, got {:?}", other),
}
}
}
#[test]
fn protocol_compat_hub_auth_success() {
let json = read_fixture("hub-auth-success");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-auth-success");
match &msg {
HubMessage::AuthSuccess {
sensor_id,
tenant_id,
capabilities,
protocol_version,
} => {
assert_eq!(sensor_id, "sensor-prod-01");
assert_eq!(tenant_id, "tenant-acme-corp");
assert_eq!(capabilities.len(), 3);
assert_eq!(protocol_version, &Some("1.0".to_string()));
}
other => panic!("Expected AuthSuccess, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_auth_success_minimal() {
let json = read_fixture("hub-auth-success-minimal");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-auth-success-minimal");
match &msg {
HubMessage::AuthSuccess {
capabilities,
protocol_version,
..
} => {
assert!(capabilities.is_empty());
assert!(protocol_version.is_none());
}
other => panic!("Expected AuthSuccess, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_auth_failed() {
let json = read_fixture("hub-auth-failed");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-auth-failed");
match &msg {
HubMessage::AuthFailed { error } => {
assert_eq!(error, "Invalid API key");
}
other => panic!("Expected AuthFailed, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_signal_ack() {
let json = read_fixture("hub-signal-ack");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-signal-ack");
match &msg {
HubMessage::SignalAck { sequence_id } => {
assert_eq!(*sequence_id, 42);
}
other => panic!("Expected SignalAck, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_batch_ack() {
let json = read_fixture("hub-batch-ack");
let msg: HubMessage = serde_json::from_str(&json).expect("Failed to deserialize hub-batch-ack");
match &msg {
HubMessage::BatchAck { count, sequence_id } => {
assert_eq!(*count, 3);
assert_eq!(*sequence_id, 43);
}
other => panic!("Expected BatchAck, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_ping() {
let json = read_fixture("hub-ping");
let msg: HubMessage = serde_json::from_str(&json).expect("Failed to deserialize hub-ping");
match &msg {
HubMessage::Ping { timestamp } => {
assert_eq!(*timestamp, 1706745600000);
}
other => panic!("Expected Ping, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_error() {
let json = read_fixture("hub-error");
let msg: HubMessage = serde_json::from_str(&json).expect("Failed to deserialize hub-error");
match &msg {
HubMessage::Error { error, code } => {
assert_eq!(error, "Rate limit exceeded");
assert_eq!(code, &Some("RATE_LIMIT".to_string()));
}
other => panic!("Expected Error, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_error_minimal() {
let json = read_fixture("hub-error-minimal");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-error-minimal");
match &msg {
HubMessage::Error { error, code } => {
assert_eq!(error, "Internal server error");
assert!(code.is_none());
}
other => panic!("Expected Error, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_blocklist_snapshot() {
let json = read_fixture("hub-blocklist-snapshot");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-blocklist-snapshot");
match &msg {
HubMessage::BlocklistSnapshot {
entries,
sequence_id,
} => {
assert_eq!(entries.len(), 2);
assert_eq!(*sequence_id, 100);
assert_eq!(entries[0].indicator, "192.168.1.100");
assert_eq!(entries[1].indicator, "t13d1516h2_malicious");
}
other => panic!("Expected BlocklistSnapshot, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_blocklist_update() {
let json = read_fixture("hub-blocklist-update");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-blocklist-update");
match &msg {
HubMessage::BlocklistUpdate {
updates,
sequence_id,
} => {
assert_eq!(updates.len(), 2);
assert_eq!(*sequence_id, 101);
}
other => panic!("Expected BlocklistUpdate, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_config_update() {
let json = read_fixture("hub-config-update");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-config-update");
match &msg {
HubMessage::ConfigUpdate { config, version } => {
assert_eq!(version, "2.1.0");
assert!(config.get("riskBasedBlockingEnabled").is_some());
}
other => panic!("Expected ConfigUpdate, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_rules_update() {
let json = read_fixture("hub-rules-update");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-rules-update");
match &msg {
HubMessage::RulesUpdate { rules, version } => {
assert_eq!(version, "3.0.0");
assert!(rules.get("rules").is_some());
}
other => panic!("Expected RulesUpdate, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_push_config() {
let json = read_fixture("hub-push-config");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-push-config");
match &msg {
HubMessage::PushConfig {
command_id,
payload,
} => {
assert_eq!(command_id, "cmd-push-config-001");
assert!(payload.config.is_some());
}
other => panic!("Expected PushConfig, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_push_rules() {
let json = read_fixture("hub-push-rules");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-push-rules");
match &msg {
HubMessage::PushRules {
command_id,
payload,
} => {
assert_eq!(command_id, "cmd-push-rules-001");
assert!(payload.get("rules").is_some());
}
other => panic!("Expected PushRules, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_restart() {
let json = read_fixture("hub-restart");
let msg: HubMessage = serde_json::from_str(&json).expect("Failed to deserialize hub-restart");
match &msg {
HubMessage::Restart {
command_id,
payload,
} => {
assert_eq!(command_id, "cmd-restart-001");
assert!(payload.get("graceful").is_some());
}
other => panic!("Expected Restart, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_collect_diagnostics() {
let json = read_fixture("hub-collect-diagnostics");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-collect-diagnostics");
match &msg {
HubMessage::CollectDiagnostics {
command_id,
payload,
} => {
assert_eq!(command_id, "cmd-diag-001");
assert!(payload.get("includeConfigDump").is_some());
}
other => panic!("Expected CollectDiagnostics, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_update() {
let json = read_fixture("hub-update");
let msg: HubMessage = serde_json::from_str(&json).expect("Failed to deserialize hub-update");
match &msg {
HubMessage::Update {
command_id,
payload,
} => {
assert_eq!(command_id, "cmd-update-001");
assert!(payload.get("targetVersion").is_some());
}
other => panic!("Expected Update, got {:?}", other),
}
}
#[test]
fn protocol_compat_hub_sync_blocklist() {
let json = read_fixture("hub-sync-blocklist");
let msg: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize hub-sync-blocklist");
match &msg {
HubMessage::SyncBlocklist {
command_id,
payload,
} => {
assert_eq!(command_id, "cmd-sync-bl-001");
assert!(payload.get("fullSync").is_some());
}
other => panic!("Expected SyncBlocklist, got {:?}", other),
}
}
#[test]
fn protocol_compat_roundtrip_sensor_signal() {
let signal = ThreatSignal::new(SignalType::IpThreat, Severity::High)
.with_source_ip("10.0.0.1")
.with_confidence(0.92)
.with_event_count(25);
let msg = SensorMessage::Signal { payload: signal };
let json = serde_json::to_string(&msg).expect("Failed to serialize SensorMessage");
let roundtrip: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize roundtrip SensorMessage");
match roundtrip {
SensorMessage::Signal { payload } => {
assert_eq!(payload.signal_type, SignalType::IpThreat);
assert_eq!(payload.severity, Severity::High);
assert_eq!(payload.source_ip, Some("10.0.0.1".to_string()));
assert_eq!(payload.confidence, 0.92);
assert_eq!(payload.event_count, Some(25));
}
other => panic!("Roundtrip produced wrong variant: {:?}", other),
}
}
#[test]
fn protocol_compat_roundtrip_hub_auth_success() {
let msg = HubMessage::AuthSuccess {
sensor_id: "sensor-rt-01".to_string(),
tenant_id: "tenant-rt".to_string(),
capabilities: vec!["signals".to_string(), "fleet-commands".to_string()],
protocol_version: Some("1.0".to_string()),
};
let json = serde_json::to_string(&msg).expect("Failed to serialize HubMessage");
let roundtrip: HubMessage =
serde_json::from_str(&json).expect("Failed to deserialize roundtrip HubMessage");
match roundtrip {
HubMessage::AuthSuccess {
sensor_id,
tenant_id,
capabilities,
protocol_version,
} => {
assert_eq!(sensor_id, "sensor-rt-01");
assert_eq!(tenant_id, "tenant-rt");
assert_eq!(capabilities.len(), 2);
assert_eq!(protocol_version, Some("1.0".to_string()));
}
other => panic!("Roundtrip produced wrong variant: {:?}", other),
}
}
#[test]
fn protocol_compat_roundtrip_all_sensor_variants() {
let variants: Vec<SensorMessage> = vec![
SensorMessage::Auth {
payload: AuthPayload {
api_key: "key".to_string(),
sensor_id: "s1".to_string(),
sensor_name: Some("Test".to_string()),
version: "1.0.0".to_string(),
protocol_version: Some("1.0".to_string()),
},
},
SensorMessage::Signal {
payload: ThreatSignal::new(SignalType::BotSignature, Severity::Low),
},
SensorMessage::SignalBatch {
payload: vec![
ThreatSignal::new(SignalType::IpThreat, Severity::High),
ThreatSignal::new(SignalType::RateAnomaly, Severity::Medium),
],
},
SensorMessage::Pong,
SensorMessage::BlocklistSync,
SensorMessage::Heartbeat {
payload: HeartbeatPayload {
timestamp: 1000,
status: "healthy".to_string(),
cpu: 50.0,
memory: 60.0,
disk: 30.0,
requests_last_minute: 100,
avg_latency_ms: 5.0,
config_hash: "hash1".to_string(),
rules_hash: "hash2".to_string(),
active_connections: None,
blocklist_size: None,
},
},
SensorMessage::CommandAck {
payload: CommandAckPayload {
command_id: "cmd-1".to_string(),
success: true,
message: None,
result: None,
},
},
];
for variant in variants {
let json = serde_json::to_string(&variant).expect("Failed to serialize");
let _roundtrip: SensorMessage =
serde_json::from_str(&json).expect("Failed to deserialize roundtrip");
}
}
#[test]
fn protocol_compat_rust_wire_format_sensor_signal() {
let signal = ThreatSignal::new(SignalType::IpThreat, Severity::High)
.with_source_ip("1.2.3.4")
.with_confidence(0.9);
let msg = SensorMessage::Signal { payload: signal };
let json = serde_json::to_string(&msg).unwrap();
assert!(
json.contains(r#""type":"signal""#),
"type tag must be kebab-case"
);
assert!(
json.contains(r#""signalType":"IP_THREAT""#),
"signalType must be SCREAMING_SNAKE_CASE"
);
assert!(
json.contains(r#""severity":"HIGH""#),
"severity must be SCREAMING_SNAKE_CASE"
);
assert!(
json.contains(r#""sourceIp":"1.2.3.4""#),
"field names must be camelCase"
);
assert!(
json.contains(r#""confidence":0.9"#),
"confidence must be a number"
);
}
#[test]
fn protocol_compat_rust_wire_format_hub_auth_success() {
let msg = HubMessage::AuthSuccess {
sensor_id: "s1".to_string(),
tenant_id: "t1".to_string(),
capabilities: vec!["signals".to_string()],
protocol_version: None,
};
let json = serde_json::to_string(&msg).unwrap();
assert!(
json.contains(r#""type":"auth-success""#),
"type tag must be kebab-case"
);
assert!(
json.contains(r#""sensorId":"s1""#),
"sensorId must be camelCase"
);
assert!(
json.contains(r#""tenantId":"t1""#),
"tenantId must be camelCase"
);
assert!(
!json.contains("protocolVersion"),
"None fields should be omitted"
);
}