#[cfg(test)]
mod tests {
use super::*;
use crate::transport::{
WebSocketClient, WebSocketClientConfig, WebSocketClientError,
message_protocol::{SyncMessage, MessageCodec, UserInfo, ServerInfo, PresenceAction},
SyncTransport,
};
use crate::crdt::{ReplicaId, CrdtType};
use std::time::{SystemTime, Duration};
use uuid::Uuid;
use tokio::time::timeout;
fn create_test_replica_id() -> ReplicaId {
ReplicaId::from(Uuid::new_v4())
}
fn create_test_config() -> WebSocketClientConfig {
WebSocketClientConfig {
url: "ws://localhost:3001/test".to_string(),
heartbeat_interval: Duration::from_secs(30),
message_timeout: Duration::from_secs(5),
reconnect_interval: Duration::from_secs(1),
max_reconnect_attempts: 3,
user_info: Some(UserInfo {
user_id: "test_user".to_string(),
username: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
avatar_url: None,
}),
}
}
fn create_test_delta_message() -> SyncMessage {
SyncMessage::Delta {
collection_id: "test-collection".to_string(),
crdt_type: CrdtType::LwwRegister,
delta: vec![1, 2, 3, 4],
timestamp: SystemTime::now(),
replica_id: create_test_replica_id(),
}
}
fn create_test_heartbeat_message() -> SyncMessage {
SyncMessage::Heartbeat {
replica_id: create_test_replica_id(),
timestamp: SystemTime::now(),
}
}
#[tokio::test]
async fn test_websocket_client_creation() {
let config = create_test_config();
let replica_id = create_test_replica_id();
let client = WebSocketClient::new(config, replica_id);
assert_eq!(client.replica_id(), replica_id);
assert!(!client.is_connected().await);
}
#[tokio::test]
async fn test_websocket_connection_lifecycle() {
let config = create_test_config();
let replica_id = create_test_replica_id();
let mut client = WebSocketClient::new(config, replica_id);
assert!(!client.is_connected().await);
let result = client.connect().await;
assert!(result.is_err());
let result = client.disconnect().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_message_serialization_roundtrip() {
let test_messages = vec![
create_test_delta_message(),
create_test_heartbeat_message(),
SyncMessage::PeerJoin {
replica_id: create_test_replica_id(),
user_info: Some(UserInfo {
user_id: "user123".to_string(),
username: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
avatar_url: None,
}),
},
SyncMessage::PeerLeave {
replica_id: create_test_replica_id(),
},
SyncMessage::Welcome {
peer_id: create_test_replica_id(),
timestamp: SystemTime::now(),
server_info: ServerInfo {
server_id: "server-001".to_string(),
version: "0.8.4".to_string(),
capabilities: vec!["crdt_sync".to_string(), "presence".to_string()],
},
},
SyncMessage::Presence {
peer_id: create_test_replica_id(),
action: PresenceAction::Join,
timestamp: SystemTime::now(),
},
SyncMessage::BinaryAck {
peer_id: create_test_replica_id(),
size: 1024,
timestamp: SystemTime::now(),
},
];
for (i, message) in test_messages.into_iter().enumerate() {
let serialized = MessageCodec::serialize(&message)
.expect(&format!("Failed to serialize message {}", i));
let deserialized = MessageCodec::deserialize(&serialized)
.expect(&format!("Failed to deserialize message {}", i));
match (&message, &deserialized) {
(SyncMessage::Delta { collection_id: id1, crdt_type: type1, delta: delta1, replica_id: rid1, .. },
SyncMessage::Delta { collection_id: id2, crdt_type: type2, delta: delta2, replica_id: rid2, .. }) => {
assert_eq!(id1, id2, "Collection ID mismatch in message {}", i);
assert_eq!(type1, type2, "CRDT type mismatch in message {}", i);
assert_eq!(delta1, delta2, "Delta data mismatch in message {}", i);
assert_eq!(rid1, rid2, "Replica ID mismatch in message {}", i);
}
(SyncMessage::Heartbeat { replica_id: rid1, .. },
SyncMessage::Heartbeat { replica_id: rid2, .. }) => {
assert_eq!(rid1, rid2, "Replica ID mismatch in message {}", i);
}
(SyncMessage::PeerJoin { replica_id: rid1, user_info: info1 },
SyncMessage::PeerJoin { replica_id: rid2, user_info: info2 }) => {
assert_eq!(rid1, rid2, "Replica ID mismatch in message {}", i);
assert_eq!(info1, info2, "User info mismatch in message {}", i);
}
(SyncMessage::PeerLeave { replica_id: rid1 },
SyncMessage::PeerLeave { replica_id: rid2 }) => {
assert_eq!(rid1, rid2, "Replica ID mismatch in message {}", i);
}
(SyncMessage::Welcome { peer_id: pid1, server_info: info1, .. },
SyncMessage::Welcome { peer_id: pid2, server_info: info2, .. }) => {
assert_eq!(pid1, pid2, "Peer ID mismatch in message {}", i);
assert_eq!(info1, info2, "Server info mismatch in message {}", i);
}
(SyncMessage::Presence { peer_id: pid1, action: action1, .. },
SyncMessage::Presence { peer_id: pid2, action: action2, .. }) => {
assert_eq!(pid1, pid2, "Peer ID mismatch in message {}", i);
assert_eq!(action1, action2, "Action mismatch in message {}", i);
}
(SyncMessage::BinaryAck { peer_id: pid1, size: size1, .. },
SyncMessage::BinaryAck { peer_id: pid2, size: size2, .. }) => {
assert_eq!(pid1, pid2, "Peer ID mismatch in message {}", i);
assert_eq!(size1, size2, "Size mismatch in message {}", i);
}
_ => panic!("Message type mismatch in message {}", i),
}
}
}
#[tokio::test]
async fn test_message_codec_error_handling() {
let invalid_json = b"invalid json";
let result = MessageCodec::deserialize(invalid_json);
assert!(result.is_err(), "Should fail to deserialize invalid JSON");
let empty_data = b"";
let result = MessageCodec::deserialize(empty_data);
assert!(result.is_err(), "Should fail to deserialize empty data");
let malformed_message = r#"{"type": "invalid_type", "version": "1.0.0"}"#;
let result = MessageCodec::deserialize(malformed_message.as_bytes());
assert!(result.is_err(), "Should fail to deserialize malformed message");
}
#[tokio::test]
async fn test_websocket_config_validation() {
let valid_config = create_test_config();
assert_eq!(valid_config.url, "ws://localhost:3001/test");
assert_eq!(valid_config.heartbeat_interval, Duration::from_secs(30));
assert_eq!(valid_config.message_timeout, Duration::from_secs(5));
assert_eq!(valid_config.reconnect_interval, Duration::from_secs(1));
assert_eq!(valid_config.max_reconnect_attempts, 3);
assert!(valid_config.user_info.is_some());
let default_config = WebSocketClientConfig::default();
assert!(!default_config.url.is_empty());
assert!(default_config.heartbeat_interval > Duration::from_secs(0));
assert!(default_config.message_timeout > Duration::from_secs(0));
assert!(default_config.reconnect_interval > Duration::from_secs(0));
assert!(default_config.max_reconnect_attempts > 0);
}
#[tokio::test]
async fn test_websocket_client_error_types() {
let config = create_test_config();
let replica_id = create_test_replica_id();
let client = WebSocketClient::new(config, replica_id);
let result = client.connect().await;
assert!(result.is_err());
match result.unwrap_err() {
WebSocketClientError::ConnectionFailed(_) => {
}
_ => panic!("Expected ConnectionFailed error"),
}
}
#[tokio::test]
async fn test_websocket_client_timeout_handling() {
let mut config = create_test_config();
config.message_timeout = Duration::from_millis(100);
let replica_id = create_test_replica_id();
let client = WebSocketClient::new(config, replica_id);
let result = timeout(Duration::from_millis(200), client.receive()).await;
assert!(result.is_err(), "Receive should timeout with no connection");
}
#[tokio::test]
async fn test_websocket_client_reconnect_config() {
let mut config = create_test_config();
config.max_reconnect_attempts = 5;
config.reconnect_interval = Duration::from_millis(500);
let replica_id = create_test_replica_id();
let client = WebSocketClient::new(config, replica_id);
assert!(!client.is_connected().await);
}
#[tokio::test]
async fn test_websocket_client_user_info() {
let user_info = UserInfo {
user_id: "user123".to_string(),
username: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
avatar_url: Some("https://example.com/avatar.jpg".to_string()),
};
let mut config = create_test_config();
config.user_info = Some(user_info.clone());
let replica_id = create_test_replica_id();
let client = WebSocketClient::new(config, replica_id);
assert_eq!(client.replica_id(), replica_id);
}
#[tokio::test]
async fn test_websocket_client_crdt_type_handling() {
let client = WebSocketClient::new(create_test_config(), create_test_replica_id());
let crdt_types = vec![
CrdtType::LwwRegister,
CrdtType::LwwMap,
CrdtType::GCounter,
CrdtType::Rga,
CrdtType::Lseq,
CrdtType::Tree,
CrdtType::Graph,
];
for crdt_type in crdt_types {
let message = SyncMessage::Delta {
collection_id: "test-collection".to_string(),
crdt_type,
delta: vec![],
timestamp: SystemTime::now(),
replica_id: create_test_replica_id(),
};
let serialized = MessageCodec::serialize(&message)
.expect(&format!("Failed to serialize {} message", format!("{:?}", crdt_type)));
let deserialized = MessageCodec::deserialize(&serialized)
.expect(&format!("Failed to deserialize {} message", format!("{:?}", crdt_type)));
if let SyncMessage::Delta { crdt_type: deserialized_type, .. } = deserialized {
assert_eq!(crdt_type, deserialized_type, "CRDT type should be preserved");
} else {
panic!("Deserialized message should be a Delta message");
}
}
}
#[tokio::test]
async fn test_websocket_client_presence_actions() {
let client = WebSocketClient::new(create_test_config(), create_test_replica_id());
let presence_actions = vec![
PresenceAction::Join,
PresenceAction::Leave,
PresenceAction::Update,
];
for action in presence_actions {
let message = SyncMessage::Presence {
peer_id: create_test_replica_id(),
action,
timestamp: SystemTime::now(),
};
let serialized = MessageCodec::serialize(&message)
.expect(&format!("Failed to serialize presence message with action {:?}", action));
let deserialized = MessageCodec::deserialize(&serialized)
.expect(&format!("Failed to deserialize presence message with action {:?}", action));
if let SyncMessage::Presence { action: deserialized_action, .. } = deserialized {
assert_eq!(action, deserialized_action, "Presence action should be preserved");
} else {
panic!("Deserialized message should be a Presence message");
}
}
}
#[tokio::test]
async fn test_websocket_client_server_info() {
let client = WebSocketClient::new(create_test_config(), create_test_replica_id());
let server_info = ServerInfo {
server_id: "server-001".to_string(),
version: "0.8.4".to_string(),
capabilities: vec![
"crdt_sync".to_string(),
"presence".to_string(),
"compression".to_string(),
"encryption".to_string(),
],
};
let message = SyncMessage::Welcome {
peer_id: create_test_replica_id(),
timestamp: SystemTime::now(),
server_info: server_info.clone(),
};
let serialized = MessageCodec::serialize(&message)
.expect("Failed to serialize welcome message with server info");
let deserialized = MessageCodec::deserialize(&serialized)
.expect("Failed to deserialize welcome message with server info");
if let SyncMessage::Welcome { server_info: deserialized_info, .. } = deserialized {
assert_eq!(server_info, deserialized_info, "Server info should be preserved");
} else {
panic!("Deserialized message should be a Welcome message");
}
}
}