test(protocol,core,bridge): add 83 unit tests for untested modules (TODO-015,018,019,021)

Protocol DTO: 32 serde roundtrip + edge case tests.
Core events: 27 event construction + variant coverage tests.
Bridge: 24 error mapping + DTO roundtrip tests.
Add serde_json dev-dependency to protocol and bridge crates.
This commit is contained in:
Edison Jwa
2026-06-11 11:05:11 +09:00
parent 2ab8d5aae2
commit e14dd73570
6 changed files with 1252 additions and 3 deletions
Generated
+2
View File
@@ -663,6 +663,7 @@ dependencies = [
"log",
"ndk-context",
"serde",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -727,6 +728,7 @@ dependencies = [
"futures",
"reqwest 0.13.4",
"serde",
"serde_json",
"thiserror 2.0.18",
"time",
"tokio",
+505 -2
View File
@@ -246,7 +246,7 @@ pub enum SessionEvent {
}
/// Bridge-safe mirror of channel-join projection sync state.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VoiceJoinSyncState {
/// Reducer is ready to accept channel actions.
Ready,
@@ -257,7 +257,7 @@ pub enum VoiceJoinSyncState {
}
/// Bridge-safe mirror of stable channel-join error codes.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VoiceJoinErrorCode {
/// Duplicate same-target join intent was coalesced.
DuplicateSameTargetCoalesced,
@@ -296,3 +296,506 @@ pub enum NetworkState {
/// OS reports no networks available.
Offline,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn network_state_equality() {
assert_eq!(NetworkState::Unknown, NetworkState::Unknown);
assert_eq!(NetworkState::Online, NetworkState::Online);
assert_eq!(NetworkState::Offline, NetworkState::Offline);
assert_ne!(NetworkState::Unknown, NetworkState::Online);
assert_ne!(NetworkState::Online, NetworkState::Offline);
}
#[test]
fn persisted_ptt_binding_empty() {
let binding = PersistedPttBinding::empty();
assert_eq!(binding.input_class, "");
assert_eq!(binding.key_label, "");
}
#[test]
fn persisted_ptt_binding_equality() {
let a = PersistedPttBinding {
input_class: "keyboard".to_string(),
key_label: "Space".to_string(),
};
let b = PersistedPttBinding {
input_class: "keyboard".to_string(),
key_label: "Space".to_string(),
};
assert_eq!(a, b);
}
#[test]
fn ptt_descriptor_snapshot_fields() {
let snap = PttDescriptorSnapshot {
level: "L0Focused".to_string(),
backend_id: "focused".to_string(),
bound_input_class: "keyboard".to_string(),
};
assert_eq!(snap.level, "L0Focused");
assert_eq!(snap.backend_id, "focused");
assert_eq!(snap.bound_input_class, "keyboard");
}
#[test]
fn session_event_connected() {
let evt = SessionEvent::Connected {
server_name: "Test Server".to_string(),
};
if let SessionEvent::Connected { server_name } = evt {
assert_eq!(server_name, "Test Server");
} else {
panic!("expected Connected variant");
}
}
#[test]
fn session_event_lost() {
let evt = SessionEvent::Lost {
reason: "timeout".to_string(),
};
if let SessionEvent::Lost { reason } = evt {
assert_eq!(reason, "timeout");
} else {
panic!("expected Lost variant");
}
}
#[test]
fn session_event_reconnecting() {
let evt = SessionEvent::Reconnecting {
attempt: 3,
delay_secs: 30,
};
if let SessionEvent::Reconnecting {
attempt,
delay_secs,
} = evt
{
assert_eq!(attempt, 3);
assert_eq!(delay_secs, 30);
} else {
panic!("expected Reconnecting variant");
}
}
#[test]
fn session_event_disconnected() {
let evt = SessionEvent::Disconnected {
reason: "user".to_string(),
};
if let SessionEvent::Disconnected { reason } = evt {
assert_eq!(reason, "user");
} else {
panic!("expected Disconnected variant");
}
}
#[test]
fn session_event_audio_started_stopped() {
let _ = SessionEvent::AudioStarted;
let _ = SessionEvent::AudioStopped;
}
#[test]
fn session_event_ptt_capability() {
let evt = SessionEvent::PttCapability {
level: "L1GlobalShortcut".to_string(),
backend_id: "global".to_string(),
bound_input_class: "keyboard".to_string(),
};
if let SessionEvent::PttCapability {
level,
backend_id,
bound_input_class,
} = evt
{
assert_eq!(level, "L1GlobalShortcut");
assert_eq!(backend_id, "global");
assert_eq!(bound_input_class, "keyboard");
} else {
panic!("expected PttCapability variant");
}
}
#[test]
fn session_event_voice_state() {
let evt = SessionEvent::VoiceState {
in_channel: true,
transmit_mode: 1,
mute: false,
release_tail_ms: 200,
current_channel_id: Some(42),
pending_target_channel_id: None,
can_join: false,
can_leave: true,
join_sync_state: VoiceJoinSyncState::Ready,
join_error_code: None,
};
if let SessionEvent::VoiceState {
in_channel,
transmit_mode,
mute,
release_tail_ms,
current_channel_id,
pending_target_channel_id,
can_join,
can_leave,
join_sync_state,
join_error_code,
} = evt
{
assert!(in_channel);
assert_eq!(transmit_mode, 1);
assert!(!mute);
assert_eq!(release_tail_ms, 200);
assert_eq!(current_channel_id, Some(42));
assert_eq!(pending_target_channel_id, None);
assert!(!can_join);
assert!(can_leave);
assert_eq!(join_sync_state, VoiceJoinSyncState::Ready);
assert!(join_error_code.is_none());
} else {
panic!("expected VoiceState variant");
}
}
#[test]
fn session_event_interruption_state() {
let evt = SessionEvent::InterruptionState {
began: true,
should_resume: false,
};
if let SessionEvent::InterruptionState {
began,
should_resume,
} = evt
{
assert!(began);
assert!(!should_resume);
} else {
panic!("expected InterruptionState variant");
}
}
#[test]
fn session_event_chat_message() {
let evt = SessionEvent::ChatMessage {
sender_id: 5,
sender_name: "Alice".to_string(),
message: "Hello".to_string(),
target: chanora_protocol::MessageTarget::Channel,
poke_strength: None,
};
if let SessionEvent::ChatMessage {
sender_id,
sender_name,
message,
target,
poke_strength,
} = evt
{
assert_eq!(sender_id, 5);
assert_eq!(sender_name, "Alice");
assert_eq!(message, "Hello");
assert_eq!(target, chanora_protocol::MessageTarget::Channel);
assert!(poke_strength.is_none());
} else {
panic!("expected ChatMessage variant");
}
}
#[test]
fn session_event_chat_message_with_poke() {
let evt = SessionEvent::ChatMessage {
sender_id: 3,
sender_name: "Bob".to_string(),
message: "".to_string(),
target: chanora_protocol::MessageTarget::Poke(7),
poke_strength: Some(chanora_protocol::PokeStrength::Suppressed),
};
if let SessionEvent::ChatMessage {
target,
poke_strength,
..
} = evt
{
assert_eq!(target, chanora_protocol::MessageTarget::Poke(7));
assert_eq!(poke_strength, Some(chanora_protocol::PokeStrength::Suppressed));
} else {
panic!("expected ChatMessage variant");
}
}
#[test]
fn session_event_server_activity() {
let evt = SessionEvent::ServerActivity {
message: "User joined channel".to_string(),
};
if let SessionEvent::ServerActivity { message } = &evt {
assert_eq!(message, "User joined channel");
} else {
panic!("expected ServerActivity variant");
}
}
#[test]
fn session_event_audio_route_changed() {
let evt = SessionEvent::AudioRouteChanged {
route: chanora_audio::AudioRoute::Speaker,
};
if let SessionEvent::AudioRouteChanged { route } = &evt {
assert_eq!(*route, chanora_audio::AudioRoute::Speaker);
} else {
panic!("expected AudioRouteChanged variant");
}
}
#[test]
fn session_event_client_moved() {
let evt = SessionEvent::ClientMoved {
client_id: 1,
new_channel_id: 2,
};
if let SessionEvent::ClientMoved {
client_id,
new_channel_id,
} = evt
{
assert_eq!(client_id, 1);
assert_eq!(new_channel_id, 2);
} else {
panic!("expected ClientMoved variant");
}
}
#[test]
fn session_event_client_joined() {
let evt = SessionEvent::ClientJoined {
client_id: 10,
channel_id: 3,
name: "NewUser".to_string(),
input_muted: false,
output_muted: true,
is_server_query: false,
talk_power: 0,
talk_power_granted: false,
};
if let SessionEvent::ClientJoined {
client_id,
channel_id,
name,
input_muted,
output_muted,
is_server_query,
talk_power,
talk_power_granted,
} = evt
{
assert_eq!(client_id, 10);
assert_eq!(channel_id, 3);
assert_eq!(name, "NewUser");
assert!(!input_muted);
assert!(output_muted);
assert!(!is_server_query);
assert_eq!(talk_power, 0);
assert!(!talk_power_granted);
} else {
panic!("expected ClientJoined variant");
}
}
#[test]
fn session_event_client_left() {
let evt = SessionEvent::ClientLeft {
client_id: 10,
name: "Departing".to_string(),
};
if let SessionEvent::ClientLeft { client_id, name } = evt {
assert_eq!(client_id, 10);
assert_eq!(name, "Departing");
} else {
panic!("expected ClientLeft variant");
}
}
#[test]
fn session_event_client_updated() {
let evt = SessionEvent::ClientUpdated {
client_id: 5,
input_muted: true,
output_muted: false,
is_server_query: true,
talk_power: 75,
talk_power_granted: true,
};
if let SessionEvent::ClientUpdated {
client_id,
input_muted,
output_muted,
is_server_query,
talk_power,
talk_power_granted,
} = evt
{
assert_eq!(client_id, 5);
assert!(input_muted);
assert!(!output_muted);
assert!(is_server_query);
assert_eq!(talk_power, 75);
assert!(talk_power_granted);
} else {
panic!("expected ClientUpdated variant");
}
}
#[test]
fn session_event_channel_added() {
let evt = SessionEvent::ChannelAdded {
id: 7,
parent: 1,
name: "Sub".to_string(),
order: 3,
has_password: true,
needed_talk_power: Some(50),
};
if let SessionEvent::ChannelAdded {
id,
parent,
name,
order,
has_password,
needed_talk_power,
} = evt
{
assert_eq!(id, 7);
assert_eq!(parent, 1);
assert_eq!(name, "Sub");
assert_eq!(order, 3);
assert!(has_password);
assert_eq!(needed_talk_power, Some(50));
} else {
panic!("expected ChannelAdded variant");
}
}
#[test]
fn session_event_channel_removed() {
let evt = SessionEvent::ChannelRemoved { id: 7 };
if let SessionEvent::ChannelRemoved { id } = evt {
assert_eq!(id, 7);
} else {
panic!("expected ChannelRemoved variant");
}
}
#[test]
fn session_event_channel_updated() {
let evt = SessionEvent::ChannelUpdated {
id: 7,
name: "Renamed".to_string(),
has_password: false,
needed_talk_power: None,
};
if let SessionEvent::ChannelUpdated {
id,
name,
has_password,
needed_talk_power,
} = evt
{
assert_eq!(id, 7);
assert_eq!(name, "Renamed");
assert!(!has_password);
assert!(needed_talk_power.is_none());
} else {
panic!("expected ChannelUpdated variant");
}
}
#[test]
fn voice_join_sync_state_variants() {
let ready = VoiceJoinSyncState::Ready;
let init = VoiceJoinSyncState::SynchronizingInitialSnapshot;
let reconnect = VoiceJoinSyncState::SynchronizingReconnect;
assert_ne!(
std::mem::discriminant(&ready),
std::mem::discriminant(&init)
);
assert_ne!(
std::mem::discriminant(&init),
std::mem::discriminant(&reconnect)
);
}
#[test]
fn voice_join_error_code_all_variants() {
let codes = [
VoiceJoinErrorCode::DuplicateSameTargetCoalesced,
VoiceJoinErrorCode::JoinAlreadyPendingDifferentTarget,
VoiceJoinErrorCode::JoinDenied,
VoiceJoinErrorCode::JoinProtocolFailure,
VoiceJoinErrorCode::JoinNetworkFailure,
VoiceJoinErrorCode::JoinTimeout,
VoiceJoinErrorCode::JoinSupersededByLeave,
VoiceJoinErrorCode::JoinStaleOutcomeIgnored,
VoiceJoinErrorCode::JoinReconciledDifferentChannel,
VoiceJoinErrorCode::JoinCommandRejectedBeforeSend,
VoiceJoinErrorCode::JoinCannotStartWhileSynchronizing,
];
for i in 0..codes.len() {
for j in 0..codes.len() {
if i == j {
assert_eq!(
std::mem::discriminant(&codes[i]),
std::mem::discriminant(&codes[j])
);
} else {
assert_ne!(
std::mem::discriminant(&codes[i]),
std::mem::discriminant(&codes[j])
);
}
}
}
}
#[test]
fn session_event_clone_preserves_fields() {
let evt = SessionEvent::Connected {
server_name: "Cloneable".to_string(),
};
let cloned = evt.clone();
if let SessionEvent::Connected { server_name } = cloned {
assert_eq!(server_name, "Cloneable");
} else {
panic!("expected Connected variant after clone");
}
}
#[test]
fn session_event_voice_state_with_join_error() {
let evt = SessionEvent::VoiceState {
in_channel: false,
transmit_mode: 0,
mute: false,
release_tail_ms: 200,
current_channel_id: None,
pending_target_channel_id: None,
can_join: true,
can_leave: false,
join_sync_state: VoiceJoinSyncState::Ready,
join_error_code: Some(VoiceJoinErrorCode::JoinDenied),
};
if let SessionEvent::VoiceState { join_error_code, .. } = evt {
assert_eq!(join_error_code, Some(VoiceJoinErrorCode::JoinDenied));
} else {
panic!("expected VoiceState variant");
}
}
}
+3
View File
@@ -36,3 +36,6 @@ ndk-context = "0.1"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
[dev-dependencies]
serde_json = "1"
+306 -1
View File
@@ -51,7 +51,7 @@ use thiserror::Error;
/// Errors raised at the bridge boundary. Production code must keep
/// these user-safe — no secrets, no protocol details, no path
/// information beyond what the redaction policy permits.
#[derive(Debug, Error, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Error, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum BridgeError {
/// The caller submitted a malformed command DTO.
#[error("invalid command: {0}")]
@@ -125,3 +125,308 @@ impl From<chanora_core::CoreError> for BridgeError {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip_json<
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
>(
value: &T,
) {
let json = serde_json::to_string(value).expect("serialize");
let back: T = serde_json::from_str(&json).expect("deserialize");
assert_eq!(&back, value, "roundtrip failed");
}
#[test]
fn bridge_error_invalid_command() {
let err = BridgeError::InvalidCommand("bad".to_string());
assert_eq!(err.to_string(), "invalid command: bad");
}
#[test]
fn bridge_error_dns_failed() {
let err = BridgeError::DnsFailed {
host: "example.com".to_string(),
reason: "timeout".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("example.com"));
assert!(msg.contains("timeout"));
}
#[test]
fn bridge_error_connection() {
let err = BridgeError::Connection("refused".to_string());
assert_eq!(err.to_string(), "connection: refused");
}
#[test]
fn bridge_error_not_connected() {
let err = BridgeError::NotConnected;
assert_eq!(err.to_string(), "not connected");
}
#[test]
fn bridge_error_already_connected() {
let err = BridgeError::AlreadyConnected;
assert_eq!(err.to_string(), "already connected");
}
#[test]
fn bridge_error_server_rejected() {
let err = BridgeError::ServerRejected {
code: 2568,
message: "insufficient permissions".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("2568"));
assert!(msg.contains("insufficient permissions"));
}
#[test]
fn bridge_error_unmapped() {
let err = BridgeError::Unmapped("mystery".to_string());
assert_eq!(err.to_string(), "unmapped: mystery");
}
#[test]
fn bridge_error_serde_roundtrip() {
roundtrip_json(&BridgeError::InvalidCommand("test".to_string()));
roundtrip_json(&BridgeError::NotConnected);
roundtrip_json(&BridgeError::AlreadyConnected);
roundtrip_json(&BridgeError::Connection("fail".to_string()));
roundtrip_json(&BridgeError::Unmapped("x".to_string()));
roundtrip_json(&BridgeError::DnsFailed {
host: "h".to_string(),
reason: "r".to_string(),
});
roundtrip_json(&BridgeError::ServerRejected {
code: 42,
message: "nope".to_string(),
});
}
#[test]
fn bridge_error_clone_preserves() {
let err = BridgeError::InvalidCommand("orig".to_string());
let cloned = err.clone();
assert_eq!(cloned.to_string(), err.to_string());
}
#[test]
fn from_core_error_not_connected() {
let core_err = chanora_core::CoreError::NotConnected;
let bridge_err: BridgeError = core_err.into();
assert!(matches!(bridge_err, BridgeError::NotConnected));
}
#[test]
fn from_core_error_already_connected() {
let core_err = chanora_core::CoreError::AlreadyConnected;
let bridge_err: BridgeError = core_err.into();
assert!(matches!(bridge_err, BridgeError::AlreadyConnected));
}
#[test]
fn from_core_error_audio_not_started() {
let core_err = chanora_core::CoreError::AudioNotStarted;
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::InvalidCommand(msg) => {
assert!(msg.contains("audio not started"));
}
other => panic!("expected InvalidCommand, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_dns_failed() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::DnsFailed {
host: "bad.host".to_string(),
reason: "no address".to_string(),
},
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::DnsFailed { host, reason } => {
assert_eq!(host, "bad.host");
assert_eq!(reason, "no address");
}
other => panic!("expected DnsFailed, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_server_rejected() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::ServerRejected {
code: 0x0501,
message: "channel password wrong".to_string(),
},
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::ServerRejected { code, message } => {
assert_eq!(code, 0x0501);
assert_eq!(message, "channel password wrong");
}
other => panic!("expected ServerRejected, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_file_transfer() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::FileTransfer("disk full".to_string()),
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("file transfer"));
assert!(msg.contains("disk full"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_generic() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::Connect("refused".to_string()),
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("refused"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_lost() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::Lost("timeout".to_string()),
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("timeout"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_invalid() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::Invalid("bad config".to_string()),
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("bad config"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_disconnected_early() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::DisconnectedEarly("premature".to_string()),
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("premature"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_identity() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::Identity("parse error".to_string()),
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("parse error"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_timeout() {
let core_err =
chanora_core::CoreError::Protocol(chanora_core::ProtocolError::Timeout);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("timeout"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_protocol_backend() {
let core_err = chanora_core::CoreError::Protocol(
chanora_core::ProtocolError::Backend("raw".to_string()),
);
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Connection(msg) => {
assert!(msg.contains("raw"));
}
other => panic!("expected Connection, got {other:?}"),
}
}
#[test]
fn from_core_error_invariant() {
let core_err = chanora_core::CoreError::Invariant("broken");
let bridge_err: BridgeError = core_err.into();
match bridge_err {
BridgeError::Unmapped(msg) => {
assert!(msg.contains("broken"));
}
other => panic!("expected Unmapped, got {other:?}"),
}
}
#[test]
fn error_surfaces_all_protocol_error_variants() {
let protocol_errors: Vec<chanora_core::ProtocolError> = vec![
chanora_core::ProtocolError::Invalid("x".into()),
chanora_core::ProtocolError::DnsFailed {
host: "h".into(),
reason: "r".into(),
},
chanora_core::ProtocolError::Connect("c".into()),
chanora_core::ProtocolError::DisconnectedEarly("d".into()),
chanora_core::ProtocolError::Lost("l".into()),
chanora_core::ProtocolError::Identity("i".into()),
chanora_core::ProtocolError::Timeout,
chanora_core::ProtocolError::ServerRejected {
code: 1,
message: "m".into(),
},
chanora_core::ProtocolError::Backend("b".into()),
chanora_core::ProtocolError::FileTransfer("f".into()),
];
for p_err in protocol_errors {
let core_err = chanora_core::CoreError::Protocol(p_err);
let bridge_err: BridgeError = core_err.into();
let msg = bridge_err.to_string();
assert!(!msg.is_empty(), "BridgeError message must not be empty");
}
}
}
+3
View File
@@ -42,3 +42,6 @@ reqwest = { version = "0.13", default-features = false, features = ["charset", "
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
# native-tls for Apple targets where aws-lc/rustls is problematic for iOS.
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] }
[dev-dependencies]
serde_json = "1"
+433
View File
@@ -262,3 +262,436 @@ pub enum ProtocolDelta {
needed_talk_power: Option<i32>,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip_json<T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
value: &T,
) {
let json = serde_json::to_string(value).expect("serialize");
let back: T = serde_json::from_str(&json).expect("deserialize");
assert_eq!(&back, value, "roundtrip failed");
}
#[test]
fn channel_id_serde_roundtrip() {
roundtrip_json(&ChannelId(0));
roundtrip_json(&ChannelId(1));
roundtrip_json(&ChannelId(u64::MAX));
}
#[test]
fn client_id_serde_roundtrip() {
roundtrip_json(&ClientId(0));
roundtrip_json(&ClientId(42));
roundtrip_json(&ClientId(u64::MAX));
}
#[test]
fn channel_id_root_is_zero() {
assert_eq!(ChannelId::ROOT, ChannelId(0));
}
#[test]
fn channel_info_serde_roundtrip() {
let info = ChannelInfo {
id: ChannelId(1),
parent: ChannelId(0),
name: "General".to_string(),
order: 0,
has_password: false,
needed_talk_power: None,
};
roundtrip_json(&info);
}
#[test]
fn channel_info_with_all_fields() {
let info = ChannelInfo {
id: ChannelId(99),
parent: ChannelId(5),
name: "AFK".to_string(),
order: -1,
has_password: true,
needed_talk_power: Some(75),
};
roundtrip_json(&info);
}
#[test]
fn channel_info_empty_name() {
let info = ChannelInfo {
id: ChannelId(1),
parent: ChannelId(0),
name: String::new(),
order: 0,
has_password: false,
needed_talk_power: None,
};
roundtrip_json(&info);
}
#[test]
fn channel_info_unicode_name() {
let info = ChannelInfo {
id: ChannelId(1),
parent: ChannelId(0),
name: "🎮 Spielsaal 🎮".to_string(),
order: 0,
has_password: false,
needed_talk_power: None,
};
roundtrip_json(&info);
}
#[test]
fn message_target_serde_roundtrip() {
roundtrip_json(&MessageTarget::Server);
roundtrip_json(&MessageTarget::Channel);
roundtrip_json(&MessageTarget::Client(12345));
roundtrip_json(&MessageTarget::Poke(67890));
}
#[test]
fn message_target_json_shape() {
let json = serde_json::to_string(&MessageTarget::Server).unwrap();
assert_eq!(json, "\"Server\"");
let json = serde_json::to_string(&MessageTarget::Client(42)).unwrap();
assert!(json.contains("\"Client\""));
assert!(json.contains("42"));
}
#[test]
fn chat_message_serde_roundtrip() {
let msg = ChatMessage {
sender_id: ClientId(1),
sender_name: "Alice".to_string(),
message: "Hello world".to_string(),
target: MessageTarget::Channel,
poke_strength: None,
};
roundtrip_json(&msg);
}
#[test]
fn chat_message_with_poke_strength() {
let msg = ChatMessage {
sender_id: ClientId(5),
sender_name: "Bob".to_string(),
message: "".to_string(),
target: MessageTarget::Poke(99),
poke_strength: Some(PokeStrength::Strong),
};
roundtrip_json(&msg);
}
#[test]
fn chat_message_unicode_content() {
let msg = ChatMessage {
sender_id: ClientId(1),
sender_name: "日本語ネーム".to_string(),
message: "🎉 こんにちは世界 🌍".to_string(),
target: MessageTarget::Server,
poke_strength: None,
};
roundtrip_json(&msg);
}
#[test]
fn server_activity_serde_roundtrip() {
let act = ServerActivity {
message: "User joined".to_string(),
};
roundtrip_json(&act);
}
#[test]
fn server_activity_empty_message() {
let act = ServerActivity {
message: String::new(),
};
roundtrip_json(&act);
}
#[test]
fn client_info_serde_roundtrip() {
let info = ClientInfo {
id: ClientId(1),
channel: ChannelId(2),
name: "Player".to_string(),
input_muted: false,
output_muted: true,
is_speaking: false,
is_server_query: false,
talk_power: 0,
talk_power_granted: false,
};
roundtrip_json(&info);
}
#[test]
fn client_info_server_query_with_talk_power() {
let info = ClientInfo {
id: ClientId(100),
channel: ChannelId(3),
name: "Bot".to_string(),
input_muted: true,
output_muted: true,
is_speaking: false,
is_server_query: true,
talk_power: 75,
talk_power_granted: true,
};
roundtrip_json(&info);
}
#[test]
fn server_snapshot_serde_roundtrip() {
let snap = ServerSnapshot {
server_name: "Test Server".to_string(),
welcome_message: "Welcome!".to_string(),
platform: "Linux".to_string(),
version: "3.13.7".to_string(),
channels: vec![
ChannelInfo {
id: ChannelId(1),
parent: ChannelId(0),
name: "Root".to_string(),
order: 0,
has_password: false,
needed_talk_power: None,
},
],
clients: vec![
ClientInfo {
id: ClientId(1),
channel: ChannelId(1),
name: "User1".to_string(),
input_muted: false,
output_muted: false,
is_speaking: false,
is_server_query: false,
talk_power: 0,
talk_power_granted: false,
},
],
own_client_id: 1,
};
roundtrip_json(&snap);
}
#[test]
fn server_snapshot_empty_channels_and_clients() {
let snap = ServerSnapshot {
server_name: String::new(),
welcome_message: String::new(),
platform: String::new(),
version: String::new(),
channels: vec![],
clients: vec![],
own_client_id: 0,
};
roundtrip_json(&snap);
}
#[test]
fn client_profile_serde_roundtrip() {
let profile = ClientProfile {
id: ClientId(1),
channel: ChannelId(2),
name: "Player".to_string(),
unique_id: "abc123".to_string(),
database_id: Some(42),
country_code: "DE".to_string(),
description: String::new(),
version: "3.5.0".to_string(),
platform: "Windows".to_string(),
created_unix_seconds: Some(1609459200),
last_connected_unix_seconds: Some(1700000000),
connections_total: Some(100),
online_seconds: Some(3600),
idle_milliseconds: Some(500),
ping_milliseconds: Some(42),
ping_deviation_milliseconds: Some(5),
client_address: String::new(),
server_groups: vec!["Admin".to_string(), "Mod".to_string()],
channel_group: "Channel Admin".to_string(),
avatar_path: String::new(),
bytes_downloaded_month: Some(1024),
bytes_uploaded_month: Some(512),
bytes_downloaded_total: Some(4096),
bytes_uploaded_total: Some(2048),
packet_loss_client_to_server_total: Some(0.01),
packet_loss_server_to_client_total: Some(0.02),
};
roundtrip_json(&profile);
}
#[test]
fn client_profile_minimal_fields() {
let profile = ClientProfile {
id: ClientId(1),
channel: ChannelId(0),
name: String::new(),
unique_id: String::new(),
database_id: None,
country_code: String::new(),
description: String::new(),
version: String::new(),
platform: String::new(),
created_unix_seconds: None,
last_connected_unix_seconds: None,
connections_total: None,
online_seconds: None,
idle_milliseconds: None,
ping_milliseconds: None,
ping_deviation_milliseconds: None,
client_address: String::new(),
server_groups: vec![],
channel_group: String::new(),
avatar_path: String::new(),
bytes_downloaded_month: None,
bytes_uploaded_month: None,
bytes_downloaded_total: None,
bytes_uploaded_total: None,
packet_loss_client_to_server_total: None,
packet_loss_server_to_client_total: None,
};
roundtrip_json(&profile);
}
#[test]
fn protocol_delta_client_moved() {
let delta = ProtocolDelta::ClientMoved {
client_id: 1,
new_channel_id: 2,
};
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_client_joined() {
let delta = ProtocolDelta::ClientJoined {
client_id: 5,
channel_id: 3,
name: "NewUser".to_string(),
input_muted: false,
output_muted: false,
is_server_query: false,
talk_power: 0,
talk_power_granted: false,
};
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_client_left() {
let delta = ProtocolDelta::ClientLeft {
client_id: 5,
name: "Departing".to_string(),
};
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_client_updated() {
let delta = ProtocolDelta::ClientUpdated {
client_id: 10,
input_muted: true,
output_muted: false,
is_server_query: false,
talk_power: 50,
talk_power_granted: true,
};
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_channel_added() {
let delta = ProtocolDelta::ChannelAdded {
id: 7,
parent: 1,
name: "New Channel".to_string(),
order: 5,
has_password: true,
needed_talk_power: Some(25),
};
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_channel_removed() {
let delta = ProtocolDelta::ChannelRemoved { id: 7 };
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_channel_updated() {
let delta = ProtocolDelta::ChannelUpdated {
id: 7,
name: "Renamed".to_string(),
has_password: false,
needed_talk_power: None,
};
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_unicode_names() {
let delta = ProtocolDelta::ClientJoined {
client_id: 1,
channel_id: 1,
name: "ユーザー".to_string(),
input_muted: false,
output_muted: false,
is_server_query: false,
talk_power: 0,
talk_power_granted: false,
};
roundtrip_json(&delta);
}
#[test]
fn protocol_delta_boundary_values() {
let delta = ProtocolDelta::ChannelAdded {
id: u64::MAX,
parent: u64::MAX,
name: String::new(),
order: i64::MIN,
has_password: true,
needed_talk_power: Some(i32::MAX),
};
roundtrip_json(&delta);
}
#[test]
fn poke_strength_serde_roundtrip() {
roundtrip_json(&PokeStrength::Strong);
roundtrip_json(&PokeStrength::Suppressed);
roundtrip_json(&PokeStrength::SuppressedOverflow);
}
#[test]
fn client_info_equality_and_clone() {
let a = ClientId(42);
let b = a;
assert_eq!(a, b);
let c = ClientId(42);
assert_eq!(a, c);
let d = ClientId(43);
assert_ne!(a, d);
}
#[test]
fn channel_id_hash_consistency() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ChannelId(1));
set.insert(ChannelId(1));
set.insert(ChannelId(2));
assert_eq!(set.len(), 2);
}
}