#![cfg(target_arch = "wasm32")]
use kalam_client::*;
use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::JsValue;
use wasm_bindgen::{closure::Closure, JsCast};
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
fn create_test_client() -> KalamClient {
KalamClient::new(
"http://localhost:2900".to_string(),
"testuser".to_string(),
"testpass".to_string(),
)
.expect("Client creation should succeed")
}
fn js_error_text(err: JsValue) -> String {
err.as_string().unwrap_or_else(|| format!("{:?}", err))
}
fn capture_connection_events() -> (
Rc<RefCell<Vec<(String, bool, Option<String>, Option<String>, Option<String>)>>>,
Closure<dyn FnMut(JsValue)>,
) {
let events = Rc::new(RefCell::new(Vec::new()));
let captured = Rc::clone(&events);
let closure = Closure::wrap(Box::new(move |event: JsValue| {
let message = js_sys::Reflect::get(&event, &"message".into())
.ok()
.and_then(|value| value.as_string())
.unwrap_or_else(|| format!("{:?}", event));
let recoverable = js_sys::Reflect::get(&event, &"recoverable".into())
.ok()
.and_then(|value| value.as_bool())
.unwrap_or(false);
let url = js_sys::Reflect::get(&event, &"url".into())
.ok()
.and_then(|value| value.as_string());
let auth_user = js_sys::Reflect::get(&event, &"authUser".into())
.ok()
.and_then(|value| value.as_string());
let hint = js_sys::Reflect::get(&event, &"hint".into())
.ok()
.and_then(|value| value.as_string());
captured.borrow_mut().push((message, recoverable, url, auth_user, hint));
}) as Box<dyn FnMut(JsValue)>);
(events, closure)
}
#[wasm_bindgen_test]
fn test_client_creation_valid() {
let client = KalamClient::new(
"http://localhost:2900".to_string(),
"testuser".to_string(),
"testpass".to_string(),
);
assert!(client.is_ok(), "Client creation should succeed with valid parameters");
}
#[wasm_bindgen_test]
fn test_client_creation_empty_url() {
let client = KalamClient::new("".to_string(), "testuser".to_string(), "testpass".to_string());
assert!(client.is_err(), "Client creation should fail with empty URL");
}
#[wasm_bindgen_test]
fn test_client_creation_empty_username() {
let client = KalamClient::new(
"http://localhost:2900".to_string(),
"".to_string(),
"testpass".to_string(),
);
assert!(client.is_err(), "Client creation should fail with empty username");
}
#[wasm_bindgen_test]
fn test_client_creation_empty_password() {
let client = KalamClient::new(
"http://localhost:2900".to_string(),
"testuser".to_string(),
"".to_string(),
);
assert!(client.is_err(), "Client creation should fail with empty password");
}
#[wasm_bindgen_test]
async fn test_connect_errors_cleanly_when_server_unavailable() {
let mut client =
KalamClient::with_jwt("http://127.0.0.1:1".to_string(), "test-token".to_string())
.expect("client creation should succeed");
let (events, on_error) = capture_connection_events();
client.on_error(on_error.as_ref().unchecked_ref::<js_sys::Function>().clone());
let result = client.connect().await;
let err = js_error_text(result.expect_err("unreachable connect should fail"));
assert!(
err.contains("WebSocket connection failed")
|| err.contains("closed")
|| err.contains("connect"),
"unexpected unreachable connect error: {err}"
);
assert_eq!(events.borrow().len(), 1, "onError should fire for unreachable server");
assert!(events.borrow()[0].0.contains("WebSocket connection failed"));
assert!(events.borrow()[0].1, "unreachable server should be recoverable");
assert_eq!(events.borrow()[0].2.as_deref(), Some("http://127.0.0.1:1"));
assert!(events.borrow()[0].4.as_deref().unwrap_or_default().contains("reachable"));
}
#[wasm_bindgen_test]
async fn test_on_error_fires_for_invalid_url_before_socket_creation() {
let mut client = KalamClient::with_jwt("http://[::1".to_string(), "test-token".to_string())
.expect("client creation should succeed");
let (events, on_error) = capture_connection_events();
client.on_connection_error(on_error.as_ref().unchecked_ref::<js_sys::Function>().clone());
let err = js_error_text(client.connect().await.expect_err("invalid URL should fail"));
assert!(
err.to_lowercase().contains("url") || err.to_lowercase().contains("invalid"),
"unexpected invalid URL error: {err}"
);
assert_eq!(events.borrow().len(), 1, "onConnectionError should fire for invalid URL");
assert!(events.borrow()[0].0.contains("Failed to connect to KalamDB"));
assert!(!events.borrow()[0].1, "invalid URL should be fatal");
assert_eq!(events.borrow()[0].2.as_deref(), Some("http://[::1"));
assert!(events.borrow()[0]
.4
.as_deref()
.unwrap_or_default()
.contains("absolute http:// or https://"));
}
#[wasm_bindgen_test]
async fn test_on_error_fires_for_auth_provider_resolution_failure() {
let mut client = KalamClient::anonymous("http://localhost:2900".to_string())
.expect("client creation should succeed");
let (events, on_error) = capture_connection_events();
client.on_error(on_error.as_ref().unchecked_ref::<js_sys::Function>().clone());
let provider = js_sys::Function::new_no_args("return { jwt: {} }; ");
client.set_auth_provider(provider);
let err =
js_error_text(client.connect().await.expect_err("invalid authProvider result should fail"));
assert!(
err.contains("authProvider result must have shape"),
"unexpected authProvider error: {err}"
);
assert_eq!(
events.borrow().len(),
1,
"onError should fire for authProvider resolution failure"
);
assert!(events.borrow()[0].0.contains("Authentication failed"));
assert!(!events.borrow()[0].1, "authProvider shape errors should be fatal");
assert_eq!(events.borrow()[0].2.as_deref(), Some("http://localhost:2900"));
assert!(events.borrow()[0]
.4
.as_deref()
.unwrap_or_default()
.contains("JWT token or auth provider"));
}
#[wasm_bindgen_test]
async fn test_disconnect() {
let mut client = create_test_client();
let _ = client.connect().await;
let result = client.disconnect().await;
assert!(result.is_ok(), "Disconnect should succeed");
assert!(!client.is_connected(), "Client should not be connected after disconnect");
}
#[wasm_bindgen_test]
async fn test_insert_rejects_invalid_table_name() {
let client = create_test_client();
let result = client
.insert("bad table name".to_string(), r#"{"title":"x"}"#.to_string())
.await;
let err = js_error_text(result.expect_err("invalid table name should fail"));
assert!(
err.contains("Table name") || err.contains("invalid character"),
"unexpected insert validation error: {err}"
);
}
#[wasm_bindgen_test]
async fn test_insert_rejects_invalid_json_payload() {
let client = create_test_client();
let result = client.insert("todos".to_string(), "{not-json}".to_string()).await;
let err = js_error_text(result.expect_err("invalid JSON should fail"));
assert!(err.contains("Invalid JSON data"), "unexpected invalid JSON error: {err}");
}
#[wasm_bindgen_test]
async fn test_insert_rejects_empty_object() {
let client = create_test_client();
let result = client.insert("todos".to_string(), "{}".to_string()).await;
let err = js_error_text(result.expect_err("empty object should fail"));
assert!(
err.contains("Cannot insert empty object"),
"unexpected empty object error: {err}"
);
}
#[wasm_bindgen_test]
async fn test_live_events_not_connected() {
let client = create_test_client();
let callback = js_sys::Function::new_no_args("");
let result = client.live_events("SELECT * FROM todos".to_string(), None, callback).await;
assert!(result.is_err(), "liveEvents should fail when not connected");
}
#[wasm_bindgen_test]
async fn test_unsubscribe_not_connected() {
let client = create_test_client();
let result = client.unsubscribe("test-subscription".to_string()).await;
assert!(result.is_err(), "Unsubscribe should fail when not connected");
}
#[wasm_bindgen_test]
async fn test_delete_rejects_invalid_row_id() {
let client = create_test_client();
let result = client.delete("todos".to_string(), "bad' OR 1=1 --".to_string()).await;
let err = js_error_text(result.expect_err("invalid row id should fail"));
assert!(
err.contains("Row ID") && (err.contains("forbidden") || err.contains("invalid")),
"unexpected row-id validation error: {err}"
);
}
#[wasm_bindgen_test]
async fn test_multiple_subscriptions_share_single_websocket() {
let mut client = create_test_client();
let connect_result = client.connect().await;
if connect_result.is_err() {
return;
}
assert!(client.is_connected(), "Client should be connected after connect()");
let callback1 =
js_sys::Function::new_with_args("data", "console.log('Subscription 1:', data);");
let callback2 =
js_sys::Function::new_with_args("data", "console.log('Subscription 2:', data);");
let callback3 =
js_sys::Function::new_with_args("data", "console.log('Subscription 3:', data);");
let sub1_result = client.live_events("SELECT * FROM todos".to_string(), None, callback1).await;
let sub2_result = client.live_events("SELECT * FROM events".to_string(), None, callback2).await;
let sub3_result =
client.live_events("SELECT * FROM messages".to_string(), None, callback3).await;
assert!(
client.is_connected(),
"Client should remain connected with multiple subscriptions"
);
if sub1_result.is_ok() && sub2_result.is_ok() && sub3_result.is_ok() {
let sub1_id = sub1_result.unwrap();
let sub2_id = sub2_result.unwrap();
let sub3_id = sub3_result.unwrap();
assert_ne!(sub1_id, sub2_id, "Subscription IDs should be unique");
assert_ne!(sub2_id, sub3_id, "Subscription IDs should be unique");
assert_ne!(sub1_id, sub3_id, "Subscription IDs should be unique");
let _ = client.unsubscribe(sub1_id).await;
assert!(client.is_connected(), "Connection should remain open after unsubscribing one");
let _ = client.unsubscribe(sub2_id).await;
assert!(client.is_connected(), "Connection should remain open after unsubscribing two");
let _ = client.unsubscribe(sub3_id).await;
assert!(
client.is_connected(),
"Connection should remain open until disconnect() is called"
);
}
let _ = client.disconnect().await;
assert!(!client.is_connected(), "Client should be disconnected after disconnect()");
}
#[wasm_bindgen_test]
async fn test_live_events_same_table_multiple_times() {
let mut client = create_test_client();
let connect_result = client.connect().await;
if connect_result.is_err() {
return;
}
let callback1 = js_sys::Function::new_with_args("data", "console.log('First:', data);");
let callback2 = js_sys::Function::new_with_args("data", "console.log('Second:', data);");
let sub1_result = client.live_events("SELECT * FROM todos".to_string(), None, callback1).await;
let sub2_result = client.live_events("SELECT * FROM todos".to_string(), None, callback2).await;
assert!(client.is_connected(), "Single WebSocket connection should be active");
let _ = client.disconnect().await;
}