#![allow(dead_code)]
#![allow(unused_variables)]
use serde::{Deserialize, Serialize};
use server_less::{server, ws};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Item {
id: u32,
name: String,
}
#[derive(Clone)]
struct TestService {
counter: std::sync::Arc<std::sync::atomic::AtomicU32>,
}
impl TestService {
fn new() -> Self {
Self {
counter: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
}
}
}
#[ws(path = "/ws")]
impl TestService {
pub fn echo(&self, message: String) -> String {
format!("Echo: {}", message)
}
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
pub fn next_id(&self) -> u32 {
self.counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
}
pub fn create_item(&self, name: String) -> Item {
let id = self
.counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Item { id, name }
}
pub fn search(&self, query: String, limit: Option<u32>) -> Vec<Item> {
let limit = limit.unwrap_or(10);
(0..limit)
.map(|i| Item {
id: i,
name: format!("{} {}", query, i),
})
.collect()
}
}
#[test]
fn test_ws_methods_generated() {
let methods = TestService::ws_methods();
assert_eq!(methods.len(), 5);
assert!(methods.contains(&"echo".to_string()));
assert!(methods.contains(&"add".to_string()));
assert!(methods.contains(&"next_id".to_string()));
assert!(methods.contains(&"create_item".to_string()));
assert!(methods.contains(&"search".to_string()));
}
#[test]
fn test_ws_handle_echo() {
let service = TestService::new();
let response =
service.ws_handle_message(r#"{"method": "echo", "params": {"message": "hello"}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], "Echo: hello");
}
#[test]
fn test_ws_handle_add() {
let service = TestService::new();
let response = service.ws_handle_message(r#"{"method": "add", "params": {"a": 5, "b": 3}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], 8);
}
#[test]
fn test_ws_handle_with_id() {
let service = TestService::new();
let response =
service.ws_handle_message(r#"{"method": "add", "params": {"a": 1, "b": 2}, "id": 42}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], 3);
assert_eq!(json["id"], 42);
}
#[test]
fn test_ws_handle_unknown_method() {
let service = TestService::new();
let response = service.ws_handle_message(r#"{"method": "unknown", "params": {}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert!(
json["error"]["message"]
.as_str()
.unwrap()
.contains("Unknown method")
);
}
#[test]
fn test_ws_handle_missing_param() {
let service = TestService::new();
let response = service.ws_handle_message(r#"{"method": "echo", "params": {}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert!(
json["error"]["message"]
.as_str()
.unwrap()
.contains("Missing required parameter")
);
}
#[test]
fn test_ws_handle_optional_param() {
let service = TestService::new();
let response =
service.ws_handle_message(r#"{"method": "search", "params": {"query": "test"}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
let items = json["result"].as_array().unwrap();
assert_eq!(items.len(), 10);
let response = service
.ws_handle_message(r#"{"method": "search", "params": {"query": "test", "limit": 3}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
let items = json["result"].as_array().unwrap();
assert_eq!(items.len(), 3);
}
#[test]
fn test_ws_handle_create_item() {
let service = TestService::new();
let response =
service.ws_handle_message(r#"{"method": "create_item", "params": {"name": "Test Item"}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"]["name"], "Test Item");
assert!(json["result"]["id"].as_u64().is_some());
}
#[test]
fn test_ws_router_created() {
let service = TestService::new();
let _router = service.ws_router();
}
#[test]
fn test_ws_invalid_json() {
let service = TestService::new();
let response = service.ws_handle_message("not valid json");
assert!(response.is_err());
assert!(response.unwrap_err().contains("Invalid JSON"));
}
#[test]
fn test_ws_handle_wrong_type_required_param() {
let service = TestService::new();
let response =
service.ws_handle_message(r#"{"method": "add", "params": {"a": "not-a-number", "b": 1}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert!(
json["error"]["message"]
.as_str()
.unwrap_or("")
.contains("Invalid parameter"),
"expected 'Invalid parameter' in error, got: {:?}",
json
);
}
#[test]
fn test_ws_handle_wrong_type_optional_param_returns_error() {
let service = TestService::new();
let response = service.ws_handle_message(
r#"{"method": "search", "params": {"query": "test", "limit": "abc"}}"#,
);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert!(
json.get("error").is_some(),
"expected an error for invalid optional param type, got: {:?}",
json
);
let msg = json["error"]["message"].as_str().unwrap_or("");
assert!(
msg.contains("limit") || msg.contains("invalid") || msg.contains("Invalid"),
"error message should mention 'limit' or 'invalid type', got: {}",
msg
);
}
#[derive(Clone)]
struct AsyncWsService;
#[ws(path = "/async-ws")]
impl AsyncWsService {
pub fn sync_echo(&self, message: String) -> String {
format!("Sync: {}", message)
}
pub async fn async_fetch(&self, url: String) -> String {
format!("Fetched: {}", url)
}
pub async fn async_compute(&self, n: i64) -> i64 {
n * n
}
}
#[test]
fn test_ws_sync_method_with_sync_handler() {
let service = AsyncWsService;
let response =
service.ws_handle_message(r#"{"method": "sync_echo", "params": {"message": "test"}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], "Sync: test");
}
#[test]
fn test_ws_async_method_with_sync_handler_returns_error() {
let service = AsyncWsService;
let response = service
.ws_handle_message(r#"{"method": "async_fetch", "params": {"url": "http://example.com"}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert!(
json["error"]["message"]
.as_str()
.unwrap()
.contains("not supported in sync context")
);
}
#[tokio::test]
async fn test_ws_sync_method_with_async_handler() {
let service = AsyncWsService;
let response = service
.ws_handle_message_async(r#"{"method": "sync_echo", "params": {"message": "async test"}}"#)
.await;
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], "Sync: async test");
}
#[tokio::test]
async fn test_ws_async_method_with_async_handler() {
let service = AsyncWsService;
let response = service
.ws_handle_message_async(
r#"{"method": "async_fetch", "params": {"url": "http://example.com"}}"#,
)
.await;
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], "Fetched: http://example.com");
}
#[tokio::test]
async fn test_ws_async_compute() {
let service = AsyncWsService;
let response = service
.ws_handle_message_async(r#"{"method": "async_compute", "params": {"n": 7}}"#)
.await;
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], 49);
}
#[tokio::test]
async fn test_ws_async_with_request_id() {
let service = AsyncWsService;
let response = service
.ws_handle_message_async(
r#"{"method": "async_compute", "params": {"n": 5}, "id": "req-123"}"#,
)
.await;
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], 25);
assert_eq!(json["id"], "req-123");
}
#[test]
fn test_ws_openapi_paths_generated() {
let paths = TestService::ws_openapi_paths();
assert_eq!(paths.len(), 1);
let ws_path = &paths[0];
assert_eq!(ws_path.path, "/ws");
assert_eq!(ws_path.method, "get");
assert!(
ws_path
.operation
.summary
.as_ref()
.unwrap()
.contains("WebSocket")
);
assert!(ws_path.operation.responses.contains_key("101"));
assert!(ws_path.operation.extra.contains_key("x-websocket-protocol"));
}
#[derive(Clone)]
struct MathWs;
#[ws]
impl MathWs {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
fn double(&self, n: i32) -> i32 {
n * 2
}
}
#[derive(Clone)]
struct EchoWs;
#[ws]
impl EchoWs {
fn echo(&self, msg: String) -> String {
format!("Echo: {}", msg)
}
}
#[derive(Clone)]
struct WsApp {
math: MathWs,
echo_svc: EchoWs,
}
#[ws]
impl WsApp {
fn ping(&self) -> String {
"pong".to_string()
}
fn math(&self) -> &MathWs {
&self.math
}
fn echo_svc(&self) -> &EchoWs {
&self.echo_svc
}
}
#[test]
fn test_ws_static_mount_methods_listed() {
let methods = WsApp::ws_methods();
assert!(methods.contains(&"ping".to_string()));
assert!(methods.contains(&"math.add".to_string()));
assert!(methods.contains(&"math.double".to_string()));
assert!(methods.contains(&"echo_svc.echo".to_string()));
}
#[test]
fn test_ws_static_mount_sync_dispatch() {
let app = WsApp {
math: MathWs,
echo_svc: EchoWs,
};
let response = app.ws_handle_message(r#"{"method":"ping","params":{}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], "pong");
let response = app.ws_handle_message(r#"{"method":"math.add","params":{"a":10,"b":5}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], 15);
let response = app.ws_handle_message(r#"{"method":"echo_svc.echo","params":{"msg":"hello"}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], "Echo: hello");
}
#[tokio::test]
async fn test_ws_static_mount_async_dispatch() {
let app = WsApp {
math: MathWs,
echo_svc: EchoWs,
};
let response = app
.ws_handle_message_async(r#"{"method":"math.double","params":{"n":21}}"#)
.await;
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], 42);
}
#[derive(Clone)]
struct WsSlugApp {
math: MathWs,
}
#[ws]
impl WsSlugApp {
fn calc(&self, id: String) -> &MathWs {
let _ = &id;
&self.math
}
}
#[test]
fn test_ws_slug_mount_methods_listed() {
let methods = WsSlugApp::ws_methods();
assert!(methods.contains(&"calc.add".to_string()));
assert!(methods.contains(&"calc.double".to_string()));
}
#[test]
fn test_ws_slug_mount_sync_dispatch() {
let app = WsSlugApp { math: MathWs };
let response =
app.ws_handle_message(r#"{"method":"calc.add","params":{"id":"calc-1","a":3,"b":4}}"#);
assert!(response.is_ok());
let json: serde_json::Value = serde_json::from_str(&response.unwrap()).unwrap();
assert_eq!(json["result"], 7);
}
#[test]
fn test_ws_mount_trait_implemented() {
use server_less::WsMount;
let methods = <MathWs as WsMount>::ws_mount_methods();
assert_eq!(methods.len(), 2);
assert!(methods.contains(&"add".to_string()));
assert!(methods.contains(&"double".to_string()));
let svc = MathWs;
let result =
<MathWs as WsMount>::ws_mount_dispatch(&svc, "add", serde_json::json!({"a": 1, "b": 2}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!(3));
}
#[derive(Clone)]
struct HiddenWsService;
#[ws]
impl HiddenWsService {
pub fn public_ws(&self) -> String {
"public".to_string()
}
#[server(hidden)]
pub fn hidden_ws(&self, x: i32) -> i32 {
x + 1
}
}
#[test]
fn test_ws_hidden_method_not_in_methods_list() {
let methods = HiddenWsService::ws_methods();
assert!(methods.contains(&"public_ws".to_string()));
assert!(!methods.contains(&"hidden_ws".to_string()));
}
#[test]
fn test_ws_hidden_method_still_dispatchable_sync() {
let svc = HiddenWsService;
let msg = r#"{"method": "hidden_ws", "params": {"x": 41}, "id": 1}"#;
let result = svc.ws_handle_message(msg);
assert!(result.is_ok());
let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
assert_eq!(response["result"], serde_json::json!(42));
}
#[tokio::test]
async fn test_ws_hidden_method_still_dispatchable_async() {
let svc = HiddenWsService;
let msg = r#"{"method": "hidden_ws", "params": {"x": 9}, "id": 1}"#;
let result = svc.ws_handle_message_async(msg).await;
assert!(result.is_ok());
let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
assert_eq!(response["result"], serde_json::json!(10));
}
#[test]
fn test_ws_hidden_method_not_in_openapi_summary() {
let paths = HiddenWsService::ws_openapi_paths();
assert_eq!(paths.len(), 1);
let path = &paths[0];
let extra_str = serde_json::to_string(&path.operation.extra).unwrap();
assert!(extra_str.contains("public_ws"), "public_ws must be in WS openapi extra");
assert!(!extra_str.contains("hidden_ws"), "hidden_ws must not be in WS openapi extra");
}