use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::vortix_core::profile::ProfileId;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum UserCommand {
Connect {
profile_id: ProfileId,
},
Disconnect {
profile_id: Option<ProfileId>,
},
Reconnect {
profile_id: Option<ProfileId>,
},
ForceDisconnect {
profile_id: Option<ProfileId>,
},
UserAnswered {
prompt_id: String,
answer: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LinkState {
Up,
Down,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ProfileChange {
Renamed {
profile_id: ProfileId,
old_display_name: String,
new_display_name: String,
},
Deleted {
profile_id: ProfileId,
},
Imported {
profile_id: ProfileId,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TunnelStatusObservation {
Active {
profile_id: ProfileId,
interface_name: String,
started_at: SystemTime,
},
Inactive {
profile_id: ProfileId,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TelemetryReport {
Ip(Option<String>),
Latency(u64),
PacketLoss(f32),
Jitter(u64),
Dns(String),
PublicIpv6(Option<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Input {
UserCommand(UserCommand),
Tick,
NetworkLinkChanged(LinkState),
TelemetryReport(TelemetryReport),
ProfileChanged(ProfileChange),
TunnelStatusObserved(TunnelStatusObservation),
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(cmd: &UserCommand) -> UserCommand {
let json = serde_json::to_string(cmd).expect("serialize");
serde_json::from_str::<UserCommand>(&json).expect("deserialize")
}
#[test]
fn disconnect_none_round_trips() {
let cmd = UserCommand::Disconnect { profile_id: None };
let back = roundtrip(&cmd);
match back {
UserCommand::Disconnect { profile_id: None } => {}
other => panic!("expected Disconnect{{None}}, got {other:?}"),
}
}
#[test]
fn disconnect_some_round_trips() {
let cmd = UserCommand::Disconnect {
profile_id: Some(ProfileId::new("corp")),
};
let back = roundtrip(&cmd);
match back {
UserCommand::Disconnect {
profile_id: Some(id),
} => assert_eq!(id.as_str(), "corp"),
other => panic!("expected Disconnect{{Some(corp)}}, got {other:?}"),
}
}
#[test]
fn reconnect_none_round_trips() {
let cmd = UserCommand::Reconnect { profile_id: None };
assert!(matches!(
roundtrip(&cmd),
UserCommand::Reconnect { profile_id: None }
));
}
#[test]
fn force_disconnect_some_round_trips() {
let cmd = UserCommand::ForceDisconnect {
profile_id: Some(ProfileId::new("home")),
};
match roundtrip(&cmd) {
UserCommand::ForceDisconnect {
profile_id: Some(id),
} => assert_eq!(id.as_str(), "home"),
other => panic!("expected ForceDisconnect{{Some}}, got {other:?}"),
}
}
#[test]
fn disconnect_serializes_as_tagged_object_not_string() {
let cmd = UserCommand::Disconnect { profile_id: None };
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(
json.contains("\"Disconnect\""),
"expected externally-tagged Disconnect variant, got: {json}"
);
assert!(
json.contains("profile_id"),
"expected struct-variant payload key, got: {json}"
);
}
#[test]
fn v1_unit_variant_payload_rejected_by_v2() {
let v1_payload = "\"Disconnect\"";
let parsed: Result<UserCommand, _> = serde_json::from_str(v1_payload);
assert!(
parsed.is_err(),
"v1 unit-variant payload `{v1_payload}` should be rejected by v2 deserializer, \
got: {parsed:?}"
);
}
}