pub mod common;
use std::time::Duration;
use futures::StreamExt;
use nodedb_types::sync::wire::{DefinitionSyncMsg, SyncFrame, SyncMessageType};
use sonic_rs::JsonValueTrait;
use common::origin::{OriginServer, connect_and_handshake};
use common::sql::OriginPgwire;
async fn wait_for_definition_frame(
ws: &mut tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
definition_type: &str,
name: &str,
action: &str,
timeout: Duration,
) -> Option<DefinitionSyncMsg> {
use tokio_tungstenite::tungstenite::Message;
let deadline = tokio::time::sleep(timeout);
tokio::pin!(deadline);
loop {
tokio::select! {
msg = ws.next() => {
match msg? {
Ok(Message::Binary(data)) => {
let frame = SyncFrame::from_bytes(&data)?;
if frame.msg_type == SyncMessageType::DefinitionSync {
let msg: DefinitionSyncMsg = frame.decode_body()?;
if msg.definition_type == definition_type
&& msg.name == name
&& msg.action == action
{
return Some(msg);
}
}
}
Ok(Message::Ping(_) | Message::Pong(_) | Message::Text(_)) => {}
_ => return None,
}
}
_ = &mut deadline => return None,
}
}
}
#[tokio::test]
async fn definition_sync_function_put() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_origin.ws_url).await;
let pg = OriginPgwire::connect().await;
pg.execute("CREATE OR REPLACE FUNCTION double_int(x INT) RETURNS INT AS SELECT x * 2")
.await;
let msg = wait_for_definition_frame(
&mut ws,
"function",
"double_int",
"put",
Duration::from_secs(5),
)
.await
.expect(
"did not receive DefinitionSync 'put' for 'double_int' within 5 s; \
check that Origin emits DefinitionSyncMsg from DDL commit path",
);
assert_eq!(msg.definition_type, "function");
assert_eq!(msg.name, "double_int");
assert_eq!(msg.action, "put");
assert!(!msg.payload.is_empty(), "put payload must not be empty");
let parsed: sonic_rs::Value =
sonic_rs::from_slice(&msg.payload).expect("payload must be valid JSON");
assert_eq!(
parsed["name"].as_str().unwrap_or(""),
"double_int",
"payload.name must match"
);
assert_eq!(
parsed["return_type"].as_str().unwrap_or(""),
"INT",
"payload.return_type must match"
);
}
#[tokio::test]
async fn definition_sync_function_delete() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_origin.ws_url).await;
let pg = OriginPgwire::connect().await;
pg.execute("CREATE OR REPLACE FUNCTION to_drop_fn(x TEXT) RETURNS TEXT AS SELECT x")
.await;
let _ = wait_for_definition_frame(
&mut ws,
"function",
"to_drop_fn",
"put",
Duration::from_secs(5),
)
.await;
pg.execute("DROP FUNCTION to_drop_fn").await;
let msg = wait_for_definition_frame(
&mut ws,
"function",
"to_drop_fn",
"delete",
Duration::from_secs(5),
)
.await
.expect("did not receive DefinitionSync 'delete' for 'to_drop_fn' within 5 s");
assert_eq!(msg.action, "delete");
assert!(
msg.payload.is_empty(),
"delete payload must be empty, got {} bytes",
msg.payload.len()
);
}
#[tokio::test]
async fn definition_sync_trigger_put() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_origin.ws_url).await;
let pg = OriginPgwire::connect().await;
pg.execute(
"CREATE COLLECTION IF NOT EXISTS trigger_test_col WITH (engine='document_schemaless')",
)
.await;
pg.execute(
"CREATE OR REPLACE TRIGGER log_insert \
AFTER INSERT ON trigger_test_col \
FOR EACH ROW AS BEGIN END",
)
.await;
let msg = wait_for_definition_frame(
&mut ws,
"trigger",
"log_insert",
"put",
Duration::from_secs(5),
)
.await
.expect("did not receive DefinitionSync 'put' for trigger 'log_insert' within 5 s");
assert_eq!(msg.definition_type, "trigger");
assert_eq!(msg.name, "log_insert");
assert_eq!(msg.action, "put");
assert!(!msg.payload.is_empty(), "put payload must not be empty");
let parsed: sonic_rs::Value =
sonic_rs::from_slice(&msg.payload).expect("payload must be valid JSON");
assert_eq!(
parsed["name"].as_str().unwrap_or(""),
"log_insert",
"payload.name must match"
);
assert_eq!(
parsed["collection"].as_str().unwrap_or(""),
"trigger_test_col",
"payload.collection must match"
);
}
#[tokio::test]
async fn definition_sync_procedure_put() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_origin.ws_url).await;
let pg = OriginPgwire::connect().await;
pg.execute("CREATE OR REPLACE PROCEDURE noop_proc() AS BEGIN END")
.await;
let msg = wait_for_definition_frame(
&mut ws,
"procedure",
"noop_proc",
"put",
Duration::from_secs(5),
)
.await
.expect("did not receive DefinitionSync 'put' for procedure 'noop_proc' within 5 s");
assert_eq!(msg.definition_type, "procedure");
assert_eq!(msg.name, "noop_proc");
assert_eq!(msg.action, "put");
assert!(!msg.payload.is_empty(), "put payload must not be empty");
let parsed: sonic_rs::Value =
sonic_rs::from_slice(&msg.payload).expect("payload must be valid JSON");
assert_eq!(
parsed["name"].as_str().unwrap_or(""),
"noop_proc",
"payload.name must match"
);
}