use sockudo_protocol::messages::{MessageData, PusherMessage};
use sonic_rs::prelude::*;
use sonic_rs::{Value, json};
fn message_to_json(message: &PusherMessage) -> Value {
sonic_rs::to_value(message).expect("Failed to serialize message")
}
#[test]
fn test_connection_established_format() {
let message = PusherMessage::connection_established("test-socket-123".to_string(), 120);
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher:connection_established");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(
json["data"].is_str(),
"Data field should be a String (JSON-encoded)"
);
let data_str = json["data"].as_str().expect("Data should be a string");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert!(parsed_data.is_object(), "Parsed data should be an object");
assert!(
parsed_data.get("socket_id").is_some(),
"Should have socket_id field"
);
assert!(
parsed_data["socket_id"].is_str(),
"socket_id should be a string"
);
assert_eq!(parsed_data["socket_id"], "test-socket-123");
assert!(
parsed_data.get("activity_timeout").is_some(),
"Should have activity_timeout field"
);
assert!(
parsed_data["activity_timeout"].is_number(),
"activity_timeout should be a number"
);
assert_eq!(parsed_data["activity_timeout"], 120);
}
#[test]
fn test_error_format() {
let message = PusherMessage::error(4001, "Application does not exist".to_string(), None);
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher:error");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(json["data"].is_object(), "Data field should be an Object");
let data = json["data"].as_object().expect("Data should be an object");
assert!(data.get(&"code").is_some(), "Should have 'code' field");
assert!(data["code"].is_number(), "Code should be a number");
assert_eq!(data["code"], 4001);
assert!(
data.get(&"message").is_some(),
"Should have 'message' field"
);
assert!(data["message"].is_str(), "Message should be a string");
assert_eq!(data["message"], "Application does not exist");
}
#[test]
fn test_signin_success_format() {
let user_data = r#"{"id":"123","name":"John"}"#.to_string();
let message = PusherMessage::signin_success(user_data);
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher:signin_success");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(json["data"].is_object(), "Data field should be an Object");
let data = json["data"].as_object().expect("Data should be an object");
assert!(
data.contains_key(&"user_data"),
"Should have user_data field"
);
assert!(!data.contains_key(&"auth"), "Should NOT have auth field");
assert!(data["user_data"].is_str(), "user_data should be a string");
assert_eq!(data["user_data"], r#"{"id":"123","name":"John"}"#);
}
#[test]
fn test_ping_format() {
let message = PusherMessage::ping();
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher:ping");
assert!(
json.get("data").is_none(),
"Ping should not have 'data' field"
);
}
#[test]
fn test_pong_format() {
let message = PusherMessage::pong();
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher:pong");
assert!(
json.get("data").is_none(),
"Pong should not have 'data' field"
);
}
#[test]
fn test_subscription_succeeded_format() {
use ahash::AHashMap;
use sockudo_protocol::messages::PresenceData;
let mut hash = AHashMap::new();
hash.insert("user1".to_string(), Some(json!({"name": "Alice"})));
hash.insert("user2".to_string(), Some(json!({"name": "Bob"})));
let presence_data = PresenceData {
ids: vec!["user1".to_string(), "user2".to_string()],
hash: hash.clone(),
count: 2,
};
let message =
PusherMessage::subscription_succeeded("presence-room".to_string(), Some(presence_data));
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher_internal:subscription_succeeded");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "presence-room");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(
json["data"].is_str(),
"Data field should be a String (JSON-encoded)"
);
let data_str = json["data"].as_str().expect("Data should be a string");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert!(parsed_data.is_object(), "Parsed data should be an object");
assert!(
parsed_data.get("presence").is_some(),
"Should have 'presence' field"
);
let presence = &parsed_data["presence"];
assert_eq!(presence["count"], 2);
assert_eq!(presence["ids"], json!(["user1", "user2"]));
assert_eq!(presence["hash"]["user1"], json!({"name": "Alice"}));
assert_eq!(presence["hash"]["user2"], json!({"name": "Bob"}));
}
#[test]
fn test_subscription_succeeded_non_presence_format() {
let message = PusherMessage::subscription_succeeded("private-channel".to_string(), None);
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher_internal:subscription_succeeded");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "private-channel");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(
json["data"].is_str(),
"Data field should be a String (JSON-encoded)"
);
let data_str = json["data"].as_str().expect("Data should be a string");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert!(parsed_data.is_object(), "Parsed data should be an object");
assert_eq!(parsed_data, json!({}));
}
#[test]
fn test_member_added_format() {
let user_info = json!({"name": "Alice", "email": "alice@example.com"});
let message = PusherMessage::member_added(
"presence-room".to_string(),
"user123".to_string(),
Some(user_info.clone()),
);
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher_internal:member_added");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "presence-room");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(
json["data"].is_str(),
"Data field should be a String (JSON-encoded)"
);
let data_str = json["data"].as_str().expect("Data should be a string");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert!(parsed_data.is_object(), "Parsed data should be an object");
assert!(
parsed_data.get("user_id").is_some(),
"Should have 'user_id' field"
);
assert!(
parsed_data["user_id"].is_str(),
"user_id should be a string"
);
assert_eq!(parsed_data["user_id"], "user123");
assert!(
parsed_data.get("user_info").is_some(),
"Should have 'user_info' field"
);
assert_eq!(parsed_data["user_info"], user_info);
}
#[test]
fn test_member_removed_format() {
let message = PusherMessage::member_removed("presence-room".to_string(), "user123".to_string());
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher_internal:member_removed");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "presence-room");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(
json["data"].is_str(),
"Data field should be a String (JSON-encoded)"
);
let data_str = json["data"].as_str().expect("Data should be a string");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert!(parsed_data.is_object(), "Parsed data should be an object");
assert!(
parsed_data.get("user_id").is_some(),
"Should have 'user_id' field"
);
assert!(
parsed_data["user_id"].is_str(),
"user_id should be a string"
);
assert_eq!(parsed_data["user_id"], "user123");
assert!(
parsed_data.get("user_info").is_none(),
"Should not have user_info"
);
}
#[test]
fn test_channel_event_format() {
let event_data = json!({"message": "Hello", "timestamp": 1234567890});
let message = PusherMessage::channel_event("my-event", "my-channel", event_data.clone());
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "my-event");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "my-channel");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(json["data"].is_str(), "Data field should be a String");
let data_str = json["data"].as_str().expect("Data should be a string");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert_eq!(parsed_data, event_data);
}
#[test]
fn test_channel_event_with_user_id() {
let event_data = json!({"message": "Hello from Alice"});
let message = PusherMessage {
event: Some("my-event".to_string()),
channel: Some("presence-room".to_string()),
data: Some(MessageData::String(event_data.to_string())),
name: None,
user_id: Some("user123".to_string()),
tags: None,
sequence: None,
conflation_key: None,
message_id: None,
stream_id: None,
serial: None,
idempotency_key: None,
extras: None,
delta_sequence: None,
delta_conflation_key: None,
};
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "my-event");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "presence-room");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(json["data"].is_str(), "Data field should be a String");
assert!(json.get("user_id").is_some(), "Should have 'user_id' field");
assert!(json["user_id"].is_str(), "user_id should be a string");
assert_eq!(json["user_id"], "user123");
}
#[test]
fn test_encrypted_channel_event_format() {
let encrypted_data = json!({
"ciphertext": "encrypted_content_here",
"nonce": "random_nonce_value"
});
let message = PusherMessage::channel_event(
"my-event",
"private-encrypted-channel",
encrypted_data.clone(),
);
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "my-event");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "private-encrypted-channel");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(
json["data"].is_str(),
"Data field should be a String (JSON-encoded)"
);
let data_str = json["data"].as_str().expect("Data should be a string");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert!(parsed_data.is_object(), "Parsed data should be an object");
assert!(
parsed_data.get("ciphertext").is_some(),
"Should have 'ciphertext' field"
);
assert!(
parsed_data["ciphertext"].is_str(),
"ciphertext should be a string"
);
assert_eq!(parsed_data["ciphertext"], "encrypted_content_here");
assert!(
parsed_data.get("nonce").is_some(),
"Should have 'nonce' field"
);
assert!(parsed_data["nonce"].is_str(), "nonce should be a string");
assert_eq!(parsed_data["nonce"], "random_nonce_value");
}
#[test]
fn test_client_event_accepts_string() {
let message = PusherMessage {
channel: Some("private-channel".to_string()),
event: Some("client-typing".to_string()),
data: Some(MessageData::String("user is typing...".to_string())),
name: None,
user_id: None,
tags: None,
sequence: None,
conflation_key: None,
message_id: None,
stream_id: None,
serial: None,
idempotency_key: None,
extras: None,
delta_sequence: None,
delta_conflation_key: None,
};
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "client-typing");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "private-channel");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(json["data"].is_str(), "Data should be a string");
assert_eq!(json["data"], "user is typing...");
}
#[test]
fn test_client_event_accepts_json() {
let message = PusherMessage {
channel: Some("private-channel".to_string()),
event: Some("client-typing".to_string()),
data: Some(MessageData::Json(
json!({"user": "alice", "status": "typing"}),
)),
name: None,
user_id: None,
tags: None,
sequence: None,
conflation_key: None,
message_id: None,
stream_id: None,
serial: None,
idempotency_key: None,
extras: None,
delta_sequence: None,
delta_conflation_key: None,
};
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "client-typing");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "private-channel");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(json["data"].is_object(), "Data should be an object");
assert!(
json["data"].get("user").is_some(),
"Should have 'user' field in data"
);
assert!(json["data"]["user"].is_str(), "User should be a string");
assert_eq!(json["data"]["user"], "alice");
assert!(
json["data"].get("status").is_some(),
"Should have 'status' field in data"
);
assert!(json["data"]["status"].is_str(), "Status should be a string");
assert_eq!(json["data"]["status"], "typing");
}
#[test]
fn test_cache_miss_event_format() {
let message = PusherMessage::cache_miss_event("cache-channel".to_string());
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "pusher:cache_miss");
assert!(json.get("channel").is_some(), "Should have 'channel' field");
assert!(json["channel"].is_str(), "Channel field should be a string");
assert_eq!(json["channel"], "cache-channel");
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(
json["data"].is_str(),
"Data field should be a String (empty JSON object)"
);
let data_str = json["data"].as_str().expect("Data should be a string");
assert_eq!(data_str, "{}", "Data should be exactly '{{}}'");
let parsed_data: Value =
sonic_rs::from_str(data_str).expect("Data string should contain valid JSON");
assert!(parsed_data.is_object(), "Parsed data should be an object");
assert_eq!(
parsed_data,
json!({}),
"Parsed data should be an empty object"
);
}
#[test]
fn test_channel_info_minimal_format() {
let response = PusherMessage::channel_info(false, None, None, None);
assert!(response.is_object(), "Response should be an object");
assert!(
response.get("occupied").is_some(),
"Should have 'occupied' field"
);
assert!(
response["occupied"].is_boolean(),
"'occupied' should be a boolean"
);
assert_eq!(response["occupied"], false);
assert!(
response.get("subscription_count").is_none(),
"Should not have 'subscription_count' when None"
);
assert!(
response.get("user_count").is_none(),
"Should not have 'user_count' when None"
);
assert!(
response.get("cache").is_none(),
"Should not have 'cache' when None"
);
let obj = response.as_object().expect("Response should be an object");
assert_eq!(obj.len(), 1, "Should have exactly 1 field when minimal");
}
#[test]
fn test_channel_info_with_subscription_count() {
let response = PusherMessage::channel_info(true, Some(42), None, None);
assert!(response.is_object(), "Response should be an object");
assert!(
response.get("occupied").is_some(),
"Should have 'occupied' field"
);
assert!(
response["occupied"].is_boolean(),
"'occupied' should be a boolean"
);
assert_eq!(response["occupied"], true);
assert!(
response.get("subscription_count").is_some(),
"Should have 'subscription_count' field"
);
assert!(
response["subscription_count"].is_u64(),
"'subscription_count' should be a number"
);
assert_eq!(response["subscription_count"], 42);
assert!(
response.get("user_count").is_none(),
"Should not have 'user_count' when None"
);
assert!(
response.get("cache").is_none(),
"Should not have 'cache' when None"
);
}
#[test]
fn test_channel_info_with_user_count() {
let response = PusherMessage::channel_info(true, Some(10), Some(8), None);
assert!(response.is_object(), "Response should be an object");
assert_eq!(response["occupied"], true);
assert_eq!(response["subscription_count"], 10);
assert_eq!(response["user_count"], 8);
assert!(
response["occupied"].is_boolean(),
"'occupied' should be a boolean"
);
assert!(
response["subscription_count"].is_u64(),
"'subscription_count' should be a number"
);
assert!(
response["user_count"].is_u64(),
"'user_count' should be a number"
);
assert!(
response.get("cache").is_none(),
"Should not have 'cache' when None"
);
}
#[test]
fn test_channel_info_with_cache_data() {
use std::time::Duration;
let cache_content = r#"{"event":"test","data":"cached"}"#.to_string();
let ttl = Duration::from_secs(3600);
let response =
PusherMessage::channel_info(true, Some(5), None, Some((cache_content.clone(), ttl)));
assert!(response.is_object(), "Response should be an object");
assert_eq!(response["occupied"], true);
assert_eq!(response["subscription_count"], 5);
assert!(response.get("cache").is_some(), "Should have 'cache' field");
assert!(response["cache"].is_object(), "'cache' should be an object");
let cache_obj = response["cache"]
.as_object()
.expect("Cache should be an object");
assert!(
cache_obj.contains_key(&"data"),
"Cache should have 'data' field"
);
assert!(
cache_obj.contains_key(&"ttl"),
"Cache should have 'ttl' field"
);
assert!(
response["cache"]["data"].is_str(),
"Cache data should be a string"
);
assert_eq!(response["cache"]["data"], cache_content);
assert!(
response["cache"]["ttl"].is_u64(),
"Cache ttl should be a number"
);
assert_eq!(response["cache"]["ttl"], 3600);
assert!(
response.get("user_count").is_none(),
"Should not have 'user_count' when None"
);
}
#[test]
fn test_channel_info_full_format() {
use std::time::Duration;
let cache_content = r#"{"message":"Hello from cache"}"#.to_string();
let ttl = Duration::from_secs(7200);
let response = PusherMessage::channel_info(
true,
Some(100),
Some(75),
Some((cache_content.clone(), ttl)),
);
assert!(response.is_object(), "Response should be an object");
assert_eq!(response["occupied"], true);
assert_eq!(response["subscription_count"], 100);
assert_eq!(response["user_count"], 75);
assert!(response["cache"].is_object(), "'cache' should be an object");
assert_eq!(response["cache"]["data"], cache_content);
assert_eq!(response["cache"]["ttl"], 7200);
let obj = response.as_object().expect("Response should be an object");
assert_eq!(
obj.len(),
4,
"Should have exactly 4 fields when all are populated"
);
assert!(
response["occupied"].is_boolean(),
"'occupied' should be a boolean"
);
assert!(
response["subscription_count"].is_u64(),
"'subscription_count' should be a number"
);
assert!(
response["user_count"].is_u64(),
"'user_count' should be a number"
);
assert!(
response["cache"]["data"].is_str(),
"Cache data should be a string"
);
assert!(
response["cache"]["ttl"].is_u64(),
"Cache ttl should be a number"
);
}
#[test]
fn test_channel_info_zero_values() {
let response = PusherMessage::channel_info(false, Some(0), Some(0), None);
assert!(response.is_object(), "Response should be an object");
assert_eq!(response["occupied"], false);
assert_eq!(response["subscription_count"], 0);
assert_eq!(response["user_count"], 0);
assert!(
response.get("subscription_count").is_some(),
"Should include subscription_count even when 0"
);
assert!(
response.get("user_count").is_some(),
"Should include user_count even when 0"
);
}
#[test]
fn test_watchlist_events_format() {
let message =
PusherMessage::watchlist_online_event(vec!["user1".to_string(), "user2".to_string()]);
let json = message_to_json(&message);
assert!(json.get("event").is_some(), "Should have 'event' field");
assert!(json["event"].is_str(), "Event field should be a string");
assert_eq!(json["event"], "online");
assert!(
json.get("channel").is_none(),
"Channel field should be omitted for watchlist events"
);
assert!(json.get("data").is_some(), "Should have 'data' field");
assert!(json["data"].is_object(), "Data should be an object");
assert!(
json["data"].get("user_ids").is_some(),
"Should have 'user_ids' field in data"
);
assert!(
json["data"]["user_ids"].is_array(),
"user_ids should be an array"
);
assert_eq!(json["data"]["user_ids"], json!(["user1", "user2"]));
}
use sockudo_protocol::messages::{ExtrasValue, MessageExtras};
use std::collections::HashMap;
#[test]
fn test_extras_round_trip_serialize_deserialize() {
let mut headers = HashMap::new();
headers.insert(
"x-region".to_string(),
ExtrasValue::String("us-east".to_string()),
);
headers.insert("priority".to_string(), ExtrasValue::Number(1.0));
headers.insert("urgent".to_string(), ExtrasValue::Bool(true));
let extras = MessageExtras {
headers: Some(headers),
ephemeral: Some(true),
idempotency_key: Some("abc-123".to_string()),
push: None,
echo: Some(false),
};
let json_str = sonic_rs::to_string(&extras).expect("serialize");
let round_tripped: MessageExtras = sonic_rs::from_str(&json_str).expect("deserialize");
assert_eq!(round_tripped.ephemeral, Some(true));
assert_eq!(round_tripped.idempotency_key.as_deref(), Some("abc-123"));
assert_eq!(round_tripped.echo, Some(false));
let h = round_tripped.headers.unwrap();
assert_eq!(
h.get("x-region"),
Some(&ExtrasValue::String("us-east".to_string()))
);
assert_eq!(h.get("priority"), Some(&ExtrasValue::Number(1.0)));
assert_eq!(h.get("urgent"), Some(&ExtrasValue::Bool(true)));
}
#[test]
fn test_extras_default_is_all_none() {
let extras = MessageExtras::default();
assert!(extras.headers.is_none());
assert!(extras.ephemeral.is_none());
assert!(extras.idempotency_key.is_none());
assert!(extras.push.is_none());
assert!(extras.echo.is_none());
}
#[test]
fn test_is_ephemeral_false_when_extras_none() {
let msg = PusherMessage::channel_event("test", "ch", json!({}));
assert!(!msg.is_ephemeral());
}
#[test]
fn test_is_ephemeral_true_when_set() {
let mut msg = PusherMessage::channel_event("test", "ch", json!({}));
msg.extras = Some(MessageExtras {
ephemeral: Some(true),
..Default::default()
});
assert!(msg.is_ephemeral());
}
#[test]
fn test_is_ephemeral_false_when_extras_present_but_not_set() {
let mut msg = PusherMessage::channel_event("test", "ch", json!({}));
msg.extras = Some(MessageExtras::default());
assert!(!msg.is_ephemeral());
}
#[test]
fn test_should_echo_returns_connection_default_when_extras_none() {
let msg = PusherMessage::channel_event("test", "ch", json!({}));
assert!(msg.should_echo(true));
assert!(!msg.should_echo(false));
}
#[test]
fn test_should_echo_true_overrides_connection_default() {
let mut msg = PusherMessage::channel_event("test", "ch", json!({}));
msg.extras = Some(MessageExtras {
echo: Some(true),
..Default::default()
});
assert!(msg.should_echo(false));
assert!(msg.should_echo(true));
}
#[test]
fn test_should_echo_false_overrides_connection_default() {
let mut msg = PusherMessage::channel_event("test", "ch", json!({}));
msg.extras = Some(MessageExtras {
echo: Some(false),
..Default::default()
});
assert!(!msg.should_echo(true));
assert!(!msg.should_echo(false));
}
#[test]
fn test_filter_headers_returns_none_when_extras_none() {
let msg = PusherMessage::channel_event("test", "ch", json!({}));
assert!(msg.filter_headers().is_none());
}
#[test]
fn test_filter_headers_returns_headers_when_set() {
let mut headers = HashMap::new();
headers.insert("env".to_string(), ExtrasValue::String("prod".to_string()));
let mut msg = PusherMessage::channel_event("test", "ch", json!({}));
msg.extras = Some(MessageExtras {
headers: Some(headers),
..Default::default()
});
let h = msg.filter_headers().unwrap();
assert_eq!(h.get("env"), Some(&ExtrasValue::String("prod".to_string())));
}
#[test]
fn test_v1_delivery_strips_extras() {
let mut msg =
PusherMessage::channel_event("test-event", "test-channel", json!({"hello": "world"}));
msg.extras = Some(MessageExtras {
ephemeral: Some(true),
echo: Some(false),
..Default::default()
});
msg.serial = Some(42);
msg.message_id = Some("msg-123".to_string());
msg.serial = None;
msg.message_id = None;
msg.extras = None;
let json = message_to_json(&msg);
assert!(
json.get("extras").is_none(),
"V1 delivery must not include extras"
);
assert!(
json.get("serial").is_none(),
"V1 delivery must not include serial"
);
assert!(
json.get("messageId").is_none(),
"V1 delivery must not include message_id"
);
}
#[test]
fn test_v2_delivery_includes_extras() {
let mut msg =
PusherMessage::channel_event("test-event", "test-channel", json!({"hello": "world"}));
msg.extras = Some(MessageExtras {
ephemeral: Some(true),
echo: Some(false),
..Default::default()
});
let json = message_to_json(&msg);
assert!(
json.get("extras").is_some(),
"V2 delivery must include extras"
);
let extras = json.get("extras").unwrap();
assert_eq!(extras["ephemeral"], true);
assert_eq!(extras["echo"], false);
}
#[test]
fn test_v2_extras_push_is_serialized_inside_extras() {
let mut msg =
PusherMessage::channel_event("test-event", "test-channel", json!({"hello": "world"}));
msg.extras = Some(MessageExtras {
push: Some(json!({
"title": "Hello",
"body": "Push body",
"templateData": {"id": "42"}
})),
..Default::default()
});
let json = message_to_json(&msg);
assert_eq!(json["extras"]["push"]["title"], "Hello");
assert_eq!(json["extras"]["push"]["templateData"]["id"], "42");
}
#[test]
fn test_should_include_extras_by_protocol() {
use sockudo_protocol::ProtocolVersion;
assert!(!PusherMessage::should_include_extras(&ProtocolVersion::V1));
assert!(PusherMessage::should_include_extras(&ProtocolVersion::V2));
}
#[test]
fn test_validate_headers_rejects_nested_objects() {
let raw = json!({
"extras": {
"headers": {
"valid_key": "string_value",
"bad_key": {"nested": true}
}
}
});
let result = MessageExtras::validate_headers_from_json(&raw);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("nested objects and arrays are not allowed")
);
}
#[test]
fn test_validate_headers_rejects_arrays() {
let raw = json!({
"extras": {
"headers": {
"bad_key": [1, 2, 3]
}
}
});
let result = MessageExtras::validate_headers_from_json(&raw);
assert!(result.is_err());
}
#[test]
fn test_validate_headers_accepts_flat_scalars() {
let raw = json!({
"extras": {
"headers": {
"str_key": "hello",
"num_key": 42,
"bool_key": true
}
}
});
let result = MessageExtras::validate_headers_from_json(&raw);
assert!(result.is_ok());
}
#[test]
fn test_validate_headers_ok_when_no_extras() {
let raw = json!({"name": "test-event"});
let result = MessageExtras::validate_headers_from_json(&raw);
assert!(result.is_ok());
}
#[test]
fn test_extras_skipped_in_serialization_when_none() {
let msg = PusherMessage::channel_event("test", "ch", json!({}));
let json = message_to_json(&msg);
assert!(
json.get("extras").is_none(),
"extras should be omitted when None"
);
}
#[test]
fn test_extras_idempotency_key() {
let mut msg = PusherMessage::channel_event("test", "ch", json!({}));
assert!(msg.extras_idempotency_key().is_none());
msg.extras = Some(MessageExtras {
idempotency_key: Some("dedup-key-1".to_string()),
..Default::default()
});
assert_eq!(msg.extras_idempotency_key(), Some("dedup-key-1"));
}