feat(auth): add authentication protocol definitions and build configuration
- Add TokenClaims message for JWT payload structure with user id, issuer, timestamps, and scopes - Implement IssueTokenRequest/Response for creating access and refresh tokens with TTL support - Create RefreshTokenRequest/Response for token rotation functionality - Define RevokeTokenRequest/Response with support for single token or user-wide revocation - Add VerifyTokenRequest/Response for validating JWT tokens with detailed claims information - Implement signing key distribution system with GetSigningKeysRequest/Response - Create TokenService gRPC service with IssueToken, RefreshToken, RevokeToken, VerifyToken, and GetSigningKeys methods - Add build.rs configuration to compile proto files using tonic_prost_build - Include channel, channel_settings, member, and permission protocol definitions for IM services - Generate Rust code bindings through pb/core.rs and pb/im.rs modules
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use imks::socket::adapter::{Adapter, AdapterError, BroadcastOptions, BroadcastFlags, BusMessage, LocalAdapter, SocketInfo};
|
||||
use imks::socket::packet::Packet;
|
||||
use imks::socket::session_store::{InMemorySessionStore, SessionInfo, SessionStoreTrait};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_adapter_add_and_del() {
|
||||
let sent_packets: dashmap::DashMap<String, Vec<Packet>> = dashmap::DashMap::new();
|
||||
let sent_packets_clone = sent_packets.clone();
|
||||
let send_fn = move |engine_sid: &str, packet: &Packet| {
|
||||
sent_packets_clone.entry(engine_sid.to_string()).or_insert_with(Vec::new).value_mut().push(packet.clone());
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let adapter = LocalAdapter::new(send_fn);
|
||||
|
||||
adapter.add("sid1", "room1", "/").await.unwrap();
|
||||
adapter.add("sid1", "room2", "/").await.unwrap();
|
||||
adapter.add("sid2", "room1", "/").await.unwrap();
|
||||
|
||||
let rooms = adapter.socket_rooms("sid1").await.unwrap();
|
||||
assert!(rooms.contains("room1"));
|
||||
assert!(rooms.contains("room2"));
|
||||
|
||||
adapter.del("sid1", "room1", "/").await.unwrap();
|
||||
let rooms = adapter.socket_rooms("sid1").await.unwrap();
|
||||
assert!(!rooms.contains("room1"));
|
||||
assert!(rooms.contains("room2"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_adapter_del_all() {
|
||||
let send_fn = move |_engine_sid: &str, _packet: &Packet| Ok(());
|
||||
|
||||
let adapter = LocalAdapter::new(send_fn);
|
||||
|
||||
adapter.add("sid1", "room1", "/").await.unwrap();
|
||||
adapter.add("sid1", "room2", "/").await.unwrap();
|
||||
|
||||
adapter.del_all("sid1", "/").await.unwrap();
|
||||
|
||||
let rooms = adapter.socket_rooms("sid1").await.unwrap();
|
||||
assert!(rooms.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_adapter_register_and_broadcast() {
|
||||
let sent_packets: Arc<dashmap::DashMap<String, Vec<Packet>>> = Arc::new(dashmap::DashMap::new());
|
||||
let sent_packets_clone = sent_packets.clone();
|
||||
let send_fn = move |engine_sid: &str, packet: &Packet| {
|
||||
sent_packets_clone.entry(engine_sid.to_string()).or_insert_with(Vec::new).value_mut().push(packet.clone());
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let adapter = LocalAdapter::new(send_fn);
|
||||
|
||||
// Register socket_sid → engine_sid mapping
|
||||
adapter.register("sid1", "engine1", "/").await.unwrap();
|
||||
adapter.register("sid2", "engine2", "/").await.unwrap();
|
||||
|
||||
let packet = Packet::event("/", serde_json::json!(["test", "hello"]), None);
|
||||
let opts = BroadcastOptions::default();
|
||||
adapter.broadcast(&packet, &opts).await.unwrap();
|
||||
|
||||
assert!(sent_packets.contains_key("engine1"));
|
||||
assert!(sent_packets.contains_key("engine2"));
|
||||
assert_eq!(sent_packets.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_adapter_broadcast_to_room() {
|
||||
let sent_packets: Arc<dashmap::DashMap<String, Vec<Packet>>> = Arc::new(dashmap::DashMap::new());
|
||||
let sent_packets_clone = sent_packets.clone();
|
||||
let send_fn = move |engine_sid: &str, packet: &Packet| {
|
||||
sent_packets_clone.entry(engine_sid.to_string()).or_insert_with(Vec::new).value_mut().push(packet.clone());
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let adapter = LocalAdapter::new(send_fn);
|
||||
|
||||
adapter.register("sid1", "engine1", "/").await.unwrap();
|
||||
adapter.register("sid2", "engine2", "/").await.unwrap();
|
||||
adapter.add("sid1", "room1", "/").await.unwrap();
|
||||
adapter.add("sid2", "room2", "/").await.unwrap();
|
||||
|
||||
let packet = Packet::event("/", serde_json::json!(["test", "hello"]), None);
|
||||
let opts = BroadcastOptions {
|
||||
rooms: HashSet::from(["room1".to_string()]),
|
||||
except: HashSet::new(),
|
||||
flags: BroadcastFlags::default(),
|
||||
};
|
||||
adapter.broadcast(&packet, &opts).await.unwrap();
|
||||
|
||||
assert!(sent_packets.contains_key("engine1"));
|
||||
assert!(!sent_packets.contains_key("engine2"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_adapter_broadcast_except() {
|
||||
let sent_packets: Arc<dashmap::DashMap<String, Vec<Packet>>> = Arc::new(dashmap::DashMap::new());
|
||||
let sent_packets_clone = sent_packets.clone();
|
||||
let send_fn = move |engine_sid: &str, packet: &Packet| {
|
||||
sent_packets_clone.entry(engine_sid.to_string()).or_insert_with(Vec::new).value_mut().push(packet.clone());
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let adapter = LocalAdapter::new(send_fn);
|
||||
|
||||
adapter.register("sid1", "engine1", "/").await.unwrap();
|
||||
adapter.register("sid2", "engine2", "/").await.unwrap();
|
||||
|
||||
let packet = Packet::event("/", serde_json::json!(["test", "hello"]), None);
|
||||
let opts = BroadcastOptions {
|
||||
rooms: HashSet::new(),
|
||||
except: HashSet::from(["sid1".to_string()]),
|
||||
flags: BroadcastFlags::default(),
|
||||
};
|
||||
adapter.broadcast(&packet, &opts).await.unwrap();
|
||||
|
||||
assert!(!sent_packets.contains_key("engine1"));
|
||||
assert!(sent_packets.contains_key("engine2"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_adapter_fetch_sockets() {
|
||||
let send_fn = move |_engine_sid: &str, _packet: &Packet| Ok(());
|
||||
|
||||
let adapter = LocalAdapter::new(send_fn);
|
||||
|
||||
adapter.register("sid1", "engine1", "/").await.unwrap();
|
||||
adapter.register("sid2", "engine2", "/").await.unwrap();
|
||||
adapter.add("sid1", "room1", "/").await.unwrap();
|
||||
adapter.add("sid2", "room2", "/").await.unwrap();
|
||||
|
||||
let opts = BroadcastOptions::default();
|
||||
let sockets = adapter.fetch_sockets(&opts).await.unwrap();
|
||||
assert_eq!(sockets.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_adapter_server_id_unique() {
|
||||
let send_fn1 = move |_engine_sid: &str, _packet: &Packet| Ok(());
|
||||
let send_fn2 = move |_engine_sid: &str, _packet: &Packet| Ok(());
|
||||
|
||||
let adapter1 = LocalAdapter::new(send_fn1);
|
||||
let adapter2 = LocalAdapter::new(send_fn2);
|
||||
|
||||
assert_ne!(adapter1.server_id(), adapter2.server_id());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_in_memory_session_store() {
|
||||
let store = InMemorySessionStore::new();
|
||||
|
||||
store.create("sid1", "polling", "server1").await.unwrap();
|
||||
assert!(store.exists("sid1").await.unwrap());
|
||||
|
||||
let info = store.get("sid1").await.unwrap().unwrap();
|
||||
assert_eq!(info.sid, "sid1");
|
||||
assert_eq!(info.transport, "polling");
|
||||
assert_eq!(info.state, "connecting");
|
||||
assert_eq!(info.server_id, "server1");
|
||||
|
||||
store.set_state("sid1", "open").await.unwrap();
|
||||
let info = store.get("sid1").await.unwrap().unwrap();
|
||||
assert_eq!(info.state, "open");
|
||||
|
||||
store.set_transport("sid1", "websocket").await.unwrap();
|
||||
let info = store.get("sid1").await.unwrap().unwrap();
|
||||
assert_eq!(info.transport, "websocket");
|
||||
|
||||
store.update_ping("sid1").await.unwrap();
|
||||
let info = store.get("sid1").await.unwrap().unwrap();
|
||||
assert!(info.last_ping > 0);
|
||||
|
||||
store.remove("sid1").await.unwrap();
|
||||
assert!(!store.exists("sid1").await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_in_memory_session_store_not_found() {
|
||||
let store = InMemorySessionStore::new();
|
||||
|
||||
let result = store.get("nonexistent").await.unwrap();
|
||||
assert!(result.is_none());
|
||||
|
||||
let result = store.set_state("nonexistent", "open").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bus_message_serialization() {
|
||||
let msg = BusMessage::Broadcast {
|
||||
namespace: "/".to_string(),
|
||||
packet: "2[\"hello\"]".to_string(),
|
||||
opts: BroadcastOptions::default(),
|
||||
server_id: "server-1".to_string(),
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_vec(&msg).unwrap();
|
||||
let decoded: BusMessage = serde_json::from_slice(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bus_message_socket_join() {
|
||||
let msg = BusMessage::SocketJoin {
|
||||
namespace: "/admin".to_string(),
|
||||
sid: "sid-1".to_string(),
|
||||
room: "room-1".to_string(),
|
||||
server_id: "server-1".to_string(),
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_vec(&msg).unwrap();
|
||||
let decoded: BusMessage = serde_json::from_slice(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bus_message_socket_leave() {
|
||||
let msg = BusMessage::SocketLeave {
|
||||
namespace: "/".to_string(),
|
||||
sid: "sid-1".to_string(),
|
||||
room: "room-1".to_string(),
|
||||
server_id: "server-1".to_string(),
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_vec(&msg).unwrap();
|
||||
let decoded: BusMessage = serde_json::from_slice(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bus_message_socket_disconnect() {
|
||||
let msg = BusMessage::SocketDisconnect {
|
||||
namespace: "/".to_string(),
|
||||
sid: "sid-1".to_string(),
|
||||
server_id: "server-1".to_string(),
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_vec(&msg).unwrap();
|
||||
let decoded: BusMessage = serde_json::from_slice(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_broadcast_options_serialization() {
|
||||
let opts = BroadcastOptions {
|
||||
rooms: HashSet::from(["room1".to_string(), "room2".to_string()]),
|
||||
except: HashSet::from(["sid1".to_string()]),
|
||||
flags: BroadcastFlags {
|
||||
local_only: true,
|
||||
broadcast: false,
|
||||
},
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_vec(&opts).unwrap();
|
||||
let decoded: BroadcastOptions = serde_json::from_slice(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.rooms, opts.rooms);
|
||||
assert_eq!(decoded.except, opts.except);
|
||||
assert_eq!(decoded.flags.local_only, opts.flags.local_only);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_info_serialization() {
|
||||
let info = SocketInfo {
|
||||
sid: "sid-1".to_string(),
|
||||
namespace: "/admin".to_string(),
|
||||
rooms: HashSet::from(["room1".to_string()]),
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_vec(&info).unwrap();
|
||||
let decoded: SocketInfo = serde_json::from_slice(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.sid, info.sid);
|
||||
assert_eq!(decoded.namespace, info.namespace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_info_serialization() {
|
||||
let info = SessionInfo {
|
||||
sid: "sid-1".to_string(),
|
||||
transport: "websocket".to_string(),
|
||||
state: "open".to_string(),
|
||||
server_id: "server-1".to_string(),
|
||||
created_at: 1234567890,
|
||||
last_ping: 1234567900,
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_vec(&info).unwrap();
|
||||
let decoded: SessionInfo = serde_json::from_slice(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.sid, info.sid);
|
||||
assert_eq!(decoded.transport, info.transport);
|
||||
assert_eq!(decoded.state, info.state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_bus_error_display() {
|
||||
let err = imks::socket::message_bus::MessageBusError::Redis("connection refused".to_string());
|
||||
assert_eq!(format!("{}", err), "Redis error: connection refused");
|
||||
|
||||
let err = imks::socket::message_bus::MessageBusError::Nats("timeout".to_string());
|
||||
assert_eq!(format!("{}", err), "NATS error: timeout");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adapter_error_display() {
|
||||
let err = AdapterError::Redis("SADD failed".to_string());
|
||||
assert_eq!(format!("{}", err), "Redis error: SADD failed");
|
||||
|
||||
let err = AdapterError::Nats("publish failed".to_string());
|
||||
assert_eq!(format!("{}", err), "NATS error: publish failed");
|
||||
|
||||
let err = AdapterError::Serialization("json error".to_string());
|
||||
assert_eq!(format!("{}", err), "Serialization error: json error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_error_display() {
|
||||
let err = imks::socket::session_store::SessionError::Redis("timeout".to_string());
|
||||
assert_eq!(format!("{}", err), "Redis error: timeout");
|
||||
|
||||
let err = imks::socket::session_store::SessionError::NotFound("sid-1".to_string());
|
||||
assert_eq!(format!("{}", err), "Session not found: sid-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_namespace() {
|
||||
assert!(imks::socket::namespace::is_valid_namespace("/"));
|
||||
assert!(imks::socket::namespace::is_valid_namespace("/admin"));
|
||||
assert!(imks::socket::namespace::is_valid_namespace("/chat/room1"));
|
||||
|
||||
assert!(!imks::socket::namespace::is_valid_namespace(""));
|
||||
assert!(!imks::socket::namespace::is_valid_namespace("admin"));
|
||||
assert!(!imks::socket::namespace::is_valid_namespace(&"/".repeat(257)));
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
use imks::engine::codec;
|
||||
use imks::engine::packet::{HandshakeData, Packet, PacketData, PacketType};
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_handshake_encoding() {
|
||||
let handshake = HandshakeData {
|
||||
sid: "lv_VI97HAXpY6yYWAAAC".to_string(),
|
||||
upgrades: vec!["websocket".to_string()],
|
||||
ping_interval: 25000,
|
||||
ping_timeout: 20000,
|
||||
max_payload: 1000000,
|
||||
};
|
||||
|
||||
let packet = Packet::open(&handshake);
|
||||
let encoded = codec::encode_packet(&packet);
|
||||
|
||||
assert!(encoded.starts_with('0'));
|
||||
assert!(encoded.contains("\"sid\":\"lv_VI97HAXpY6yYWAAAC\""));
|
||||
assert!(encoded.contains("\"upgrades\":[\"websocket\"]"));
|
||||
assert!(encoded.contains("\"pingInterval\":25000"));
|
||||
assert!(encoded.contains("\"pingTimeout\":20000"));
|
||||
assert!(encoded.contains("\"maxPayload\":1000000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_packet_types() {
|
||||
let open = Packet::open(&HandshakeData {
|
||||
sid: "test".to_string(),
|
||||
upgrades: vec![],
|
||||
ping_interval: 25000,
|
||||
ping_timeout: 20000,
|
||||
max_payload: 1000000,
|
||||
});
|
||||
assert_eq!(open.packet_type, PacketType::Open);
|
||||
|
||||
let close = Packet::close();
|
||||
assert_eq!(close.packet_type, PacketType::Close);
|
||||
|
||||
let ping = Packet::ping("test");
|
||||
assert_eq!(ping.packet_type, PacketType::Ping);
|
||||
|
||||
let pong = Packet::pong("test");
|
||||
assert_eq!(pong.packet_type, PacketType::Pong);
|
||||
|
||||
let msg = Packet::message_text("hello");
|
||||
assert_eq!(msg.packet_type, PacketType::Message);
|
||||
|
||||
let upgrade = Packet::upgrade();
|
||||
assert_eq!(upgrade.packet_type, PacketType::Upgrade);
|
||||
|
||||
let noop = Packet::noop();
|
||||
assert_eq!(noop.packet_type, PacketType::Noop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_polling_payload() {
|
||||
let packets = vec![
|
||||
Packet::message_text("hello"),
|
||||
Packet::ping(""),
|
||||
Packet::message_text("world"),
|
||||
];
|
||||
|
||||
let encoded = codec::encode_payload(&packets);
|
||||
assert_eq!(encoded, "4hello\x1e2\x1e4world");
|
||||
|
||||
let decoded = codec::decode_payload(&encoded).unwrap();
|
||||
assert_eq!(decoded.len(), 3);
|
||||
assert_eq!(decoded[0].packet_type, PacketType::Message);
|
||||
assert_eq!(decoded[1].packet_type, PacketType::Ping);
|
||||
assert_eq!(decoded[2].packet_type, PacketType::Message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_binary_encoding() {
|
||||
let packet = Packet::message_binary(vec![0x01, 0x02, 0x03, 0x04]);
|
||||
let encoded = codec::encode_packet(&packet);
|
||||
assert_eq!(encoded, "4bAQIDBA==");
|
||||
|
||||
let decoded = codec::decode_packet(&encoded).unwrap();
|
||||
assert_eq!(decoded.packet_type, PacketType::Message);
|
||||
assert_eq!(decoded.data, PacketData::Binary(vec![0x01, 0x02, 0x03, 0x04]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_webtransport_header_encoding() {
|
||||
let header = codec::encode_webtransport_header(6, false);
|
||||
assert_eq!(header, vec![0x06]);
|
||||
|
||||
let header = codec::encode_webtransport_header(200, true);
|
||||
assert_eq!(header.len(), 3);
|
||||
assert_eq!(header[0], 0x80 | 126);
|
||||
|
||||
let header = codec::encode_webtransport_header(70000, false);
|
||||
assert_eq!(header.len(), 9);
|
||||
assert_eq!(header[0], 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_webtransport_header_decoding() {
|
||||
let header = vec![0x06];
|
||||
let (len, is_binary) = codec::decode_webtransport_header(&header).unwrap();
|
||||
assert_eq!(len, 6);
|
||||
assert!(!is_binary);
|
||||
|
||||
let header = vec![0x80 | 126, 0x00, 0xC8];
|
||||
let (len, is_binary) = codec::decode_webtransport_header(&header).unwrap();
|
||||
assert_eq!(len, 200);
|
||||
assert!(is_binary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_probe_ping_pong() {
|
||||
let ping = Packet::ping("probe");
|
||||
let encoded = codec::encode_packet(&ping);
|
||||
assert_eq!(encoded, "2probe");
|
||||
|
||||
let decoded = codec::decode_packet(&encoded).unwrap();
|
||||
assert_eq!(decoded.packet_type, PacketType::Ping);
|
||||
assert_eq!(decoded.data, PacketData::Text("probe".to_string()));
|
||||
|
||||
let pong = Packet::pong("probe");
|
||||
let encoded = codec::encode_packet(&pong);
|
||||
assert_eq!(encoded, "3probe");
|
||||
|
||||
let decoded = codec::decode_packet(&encoded).unwrap();
|
||||
assert_eq!(decoded.packet_type, PacketType::Pong);
|
||||
assert_eq!(decoded.data, PacketData::Text("probe".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_upgrade_packet() {
|
||||
let upgrade = Packet::upgrade();
|
||||
let encoded = codec::encode_packet(&upgrade);
|
||||
assert_eq!(encoded, "5");
|
||||
|
||||
let decoded = codec::decode_packet(&encoded).unwrap();
|
||||
assert_eq!(decoded.packet_type, PacketType::Upgrade);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_noop_packet() {
|
||||
let noop = Packet::noop();
|
||||
let encoded = codec::encode_packet(&noop);
|
||||
assert_eq!(encoded, "6");
|
||||
|
||||
let decoded = codec::decode_packet(&encoded).unwrap();
|
||||
assert_eq!(decoded.packet_type, PacketType::Noop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_io_close_packet() {
|
||||
let close = Packet::close();
|
||||
let encoded = codec::encode_packet(&close);
|
||||
assert_eq!(encoded, "1");
|
||||
|
||||
let decoded = codec::decode_packet(&encoded).unwrap();
|
||||
assert_eq!(decoded.packet_type, PacketType::Close);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use imks::engine::session::{generate_sid, SessionState, SessionStore, TransportType};
|
||||
|
||||
#[test]
|
||||
fn test_session_store_create_and_get() {
|
||||
let store = SessionStore::new();
|
||||
let sid = generate_sid();
|
||||
|
||||
let _rx = store.create(sid.clone(), TransportType::Polling);
|
||||
|
||||
assert!(store.exists(&sid));
|
||||
assert!(store.get(&sid).is_some());
|
||||
assert_eq!(store.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_store_remove() {
|
||||
let store = SessionStore::new();
|
||||
let sid = generate_sid();
|
||||
|
||||
let _rx = store.create(sid.clone(), TransportType::Polling);
|
||||
assert!(store.exists(&sid));
|
||||
|
||||
store.remove(&sid);
|
||||
assert!(!store.exists(&sid));
|
||||
assert!(store.get(&sid).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_store_multiple_sessions() {
|
||||
let store = SessionStore::new();
|
||||
|
||||
let sid1 = generate_sid();
|
||||
let sid2 = generate_sid();
|
||||
let sid3 = generate_sid();
|
||||
|
||||
let _rx1 = store.create(sid1.clone(), TransportType::Polling);
|
||||
let _rx2 = store.create(sid2.clone(), TransportType::WebSocket);
|
||||
let _rx3 = store.create(sid3.clone(), TransportType::WebTransport);
|
||||
|
||||
assert_eq!(store.len(), 3);
|
||||
assert!(store.exists(&sid1));
|
||||
assert!(store.exists(&sid2));
|
||||
assert!(store.exists(&sid3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_sid_uniqueness() {
|
||||
let sids: Vec<String> = (0..100).map(|_| generate_sid()).collect();
|
||||
|
||||
let unique_sids: std::collections::HashSet<String> = sids.into_iter().collect();
|
||||
assert_eq!(unique_sids.len(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_sid_format() {
|
||||
let sid = generate_sid();
|
||||
|
||||
assert_eq!(sid.len(), 20);
|
||||
assert!(sid.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_session_state_transitions() {
|
||||
let store = SessionStore::new();
|
||||
let sid = generate_sid();
|
||||
|
||||
let _rx = store.create(sid.clone(), TransportType::Polling);
|
||||
|
||||
if let Some(session) = store.get(&sid) {
|
||||
let mut session = session.write().await;
|
||||
assert_eq!(session.state, SessionState::Connecting);
|
||||
|
||||
session.set_state(SessionState::Open);
|
||||
assert_eq!(session.state, SessionState::Open);
|
||||
|
||||
session.set_state(SessionState::Upgrading);
|
||||
assert_eq!(session.state, SessionState::Upgrading);
|
||||
|
||||
session.set_state(SessionState::Open);
|
||||
assert_eq!(session.state, SessionState::Open);
|
||||
|
||||
session.set_state(SessionState::Closing);
|
||||
assert_eq!(session.state, SessionState::Closing);
|
||||
|
||||
session.set_state(SessionState::Closed);
|
||||
assert_eq!(session.state, SessionState::Closed);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_session_transport_change() {
|
||||
let store = SessionStore::new();
|
||||
let sid = generate_sid();
|
||||
|
||||
let _rx = store.create(sid.clone(), TransportType::Polling);
|
||||
|
||||
if let Some(session) = store.get(&sid) {
|
||||
let mut session = session.write().await;
|
||||
assert_eq!(session.transport, TransportType::Polling);
|
||||
|
||||
session.set_transport(TransportType::WebSocket);
|
||||
assert_eq!(session.transport, TransportType::WebSocket);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_session_ping_update() {
|
||||
let store = SessionStore::new();
|
||||
let sid = generate_sid();
|
||||
|
||||
let _rx = store.create(sid.clone(), TransportType::Polling);
|
||||
|
||||
if let Some(session) = store.get(&sid) {
|
||||
let mut session = session.write().await;
|
||||
let initial_ping = session.last_ping;
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
session.update_ping();
|
||||
|
||||
assert!(session.last_ping > initial_ping);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transport_type_as_str() {
|
||||
assert_eq!(TransportType::Polling.as_str(), "polling");
|
||||
assert_eq!(TransportType::WebSocket.as_str(), "websocket");
|
||||
assert_eq!(TransportType::WebTransport.as_str(), "webtransport");
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use imks::socket::packet::{Packet, PacketType};
|
||||
use imks::socket::parser;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_connect_encoding() {
|
||||
let packet = Packet::connect("/", None);
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "0");
|
||||
|
||||
let packet = Packet::connect("/admin", Some(json!({"sid": "abc123"})));
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "0/admin,{\"sid\":\"abc123\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_disconnect_encoding() {
|
||||
let packet = Packet::disconnect("/");
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "1");
|
||||
|
||||
let packet = Packet::disconnect("/admin");
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "1/admin,");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_event_encoding() {
|
||||
let packet = Packet::event("/", json!(["foo"]), None);
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "2[\"foo\"]");
|
||||
|
||||
let packet = Packet::event("/admin", json!(["bar"]), None);
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "2/admin,[\"bar\"]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_event_with_ack_encoding() {
|
||||
let packet = Packet::event("/", json!(["foo"]), Some(12));
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "212[\"foo\"]");
|
||||
|
||||
let packet = Packet::event("/admin", json!(["bar"]), Some(13));
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "2/admin,13[\"bar\"]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_ack_encoding() {
|
||||
let packet = Packet::ack("/", json!([]), 12);
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "312[]");
|
||||
|
||||
let packet = Packet::ack("/admin", json!(["bar"]), 13);
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "3/admin,13[\"bar\"]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_connect_error_encoding() {
|
||||
let packet = Packet::connect_error("/", "Not authorized");
|
||||
let encoded = parser::encode(&packet);
|
||||
assert_eq!(encoded, "4{\"message\":\"Not authorized\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_connect_decoding() {
|
||||
let packet = parser::decode("0").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Connect);
|
||||
assert_eq!(packet.namespace, "/");
|
||||
assert!(packet.data.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_connect_with_namespace_decoding() {
|
||||
let packet = parser::decode("0/admin,{\"sid\":\"abc\"}").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Connect);
|
||||
assert_eq!(packet.namespace, "/admin");
|
||||
assert!(packet.data.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_disconnect_decoding() {
|
||||
let packet = parser::decode("1").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Disconnect);
|
||||
assert_eq!(packet.namespace, "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_disconnect_with_namespace_decoding() {
|
||||
let packet = parser::decode("1/admin,").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Disconnect);
|
||||
assert_eq!(packet.namespace, "/admin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_event_decoding() {
|
||||
let packet = parser::decode("2[\"foo\"]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Event);
|
||||
assert_eq!(packet.namespace, "/");
|
||||
assert_eq!(packet.data, Some(json!(["foo"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_event_with_namespace_decoding() {
|
||||
let packet = parser::decode("2/admin,[\"bar\"]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Event);
|
||||
assert_eq!(packet.namespace, "/admin");
|
||||
assert_eq!(packet.data, Some(json!(["bar"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_event_with_ack_decoding() {
|
||||
let packet = parser::decode("212[\"foo\"]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Event);
|
||||
assert_eq!(packet.id, Some(12));
|
||||
assert_eq!(packet.data, Some(json!(["foo"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_ack_decoding() {
|
||||
let packet = parser::decode("312[]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Ack);
|
||||
assert_eq!(packet.id, Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_connect_error_decoding() {
|
||||
let packet = parser::decode("4{\"message\":\"Not authorized\"}").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::ConnectError);
|
||||
assert_eq!(packet.data, Some(json!({"message": "Not authorized"})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_binary_event_encoding() {
|
||||
let packet = Packet::binary_event(
|
||||
"/",
|
||||
json!(["baz", {"_placeholder": true, "num": 0}]),
|
||||
None,
|
||||
vec![vec![0x01, 0x02, 0x03, 0x04]],
|
||||
);
|
||||
let encoded = parser::encode(&packet);
|
||||
assert!(encoded.starts_with("51-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_binary_ack_encoding() {
|
||||
let packet = Packet::binary_ack(
|
||||
"/",
|
||||
json!(["bar", {"_placeholder": true, "num": 0}]),
|
||||
15,
|
||||
vec![vec![0x01, 0x02, 0x03, 0x04]],
|
||||
);
|
||||
let encoded = parser::encode(&packet);
|
||||
assert!(encoded.starts_with("61-"));
|
||||
assert!(encoded.contains("15"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_roundtrip() {
|
||||
let original = Packet::event("/admin", json!(["hello", "world"]), Some(42));
|
||||
let encoded = parser::encode(&original);
|
||||
let decoded = parser::decode(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.packet_type, original.packet_type);
|
||||
assert_eq!(decoded.namespace, original.namespace);
|
||||
assert_eq!(decoded.id, original.id);
|
||||
assert_eq!(decoded.data, original.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_namespace_handling() {
|
||||
let namespaces = vec!["/", "/admin", "/chat", "/api/v1"];
|
||||
|
||||
for ns in namespaces {
|
||||
let packet = Packet::event(ns, json!(["test"]), None);
|
||||
let encoded = parser::encode(&packet);
|
||||
let decoded = parser::decode(&encoded).unwrap();
|
||||
assert_eq!(decoded.namespace, ns);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_socket_io_complex_data() {
|
||||
let complex_data = json!([
|
||||
"event_name",
|
||||
{
|
||||
"user": {
|
||||
"id": 123,
|
||||
"name": "test",
|
||||
"roles": ["admin", "user"]
|
||||
},
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
]);
|
||||
|
||||
let packet = Packet::event("/", complex_data.clone(), None);
|
||||
let encoded = parser::encode(&packet);
|
||||
let decoded = parser::decode(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.data, Some(complex_data));
|
||||
}
|
||||
Reference in New Issue
Block a user