mod common;
use common::native_harness::{NativeTestServer, do_handshake, read_frame, write_frame};
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use nodedb_types::protocol::request_fields::RequestFields;
use nodedb_types::protocol::text_fields::TextFields;
use nodedb_types::protocol::{
CAP_FTS, CAP_MSGPACK, CAP_SPATIAL, CAP_STREAMING, HelloErrorCode, HelloFrame, MAX_FRAME_SIZE,
NativeRequest, NativeResponse, OpCode, PROTO_VERSION_MAX, PROTO_VERSION_MIN,
};
#[tokio::test]
async fn version_handshake_v1_accepted() {
let server = NativeTestServer::start().await;
let hello = HelloFrame {
proto_min: 1,
proto_max: 1,
capabilities: CAP_STREAMING | CAP_MSGPACK,
};
let result = do_handshake(server.addr, &hello).await;
server.shutdown().await;
let (_stream, ack) = result.expect("handshake should succeed for v1 client");
assert_eq!(ack.proto_version, 1, "negotiated version must be 1");
assert!(
ack.server_version.contains("NodeDB"),
"server_version '{}' missing 'NodeDB'",
ack.server_version
);
}
#[tokio::test]
async fn version_handshake_v0_rejected() {
if PROTO_VERSION_MIN == 0 {
return;
}
let server = NativeTestServer::start().await;
let hello = HelloFrame {
proto_min: 0,
proto_max: 0,
capabilities: 0,
};
let result = do_handshake(server.addr, &hello).await;
server.shutdown().await;
let err_frame = result.expect_err("v0-only client must be rejected");
assert_eq!(
err_frame.code,
HelloErrorCode::VersionMismatch,
"error code must be VersionMismatch, got {:?}",
err_frame.code
);
}
#[tokio::test]
async fn version_handshake_future_version_rejected() {
let server = NativeTestServer::start().await;
let hello = HelloFrame {
proto_min: PROTO_VERSION_MAX.saturating_add(1),
proto_max: PROTO_VERSION_MAX.saturating_add(5),
capabilities: 0,
};
let result = do_handshake(server.addr, &hello).await;
server.shutdown().await;
let err_frame = result.expect_err("future-version-only client must be rejected");
assert_eq!(
err_frame.code,
HelloErrorCode::VersionMismatch,
"error code must be VersionMismatch"
);
}
#[tokio::test]
async fn capability_bits_negotiated() {
let server = NativeTestServer::start().await;
let client_caps = CAP_STREAMING | CAP_FTS | CAP_SPATIAL;
let hello = HelloFrame {
proto_min: 1,
proto_max: 1,
capabilities: client_caps,
};
let result = do_handshake(server.addr, &hello).await;
server.shutdown().await;
let (_stream, ack) = result.expect("handshake ok");
assert_ne!(
ack.capabilities, 0,
"server must advertise at least one capability"
);
let intersection = ack.capabilities & client_caps;
assert_ne!(
intersection, 0,
"at least one capability bit must be in the intersection"
);
let rejected = CAP_MSGPACK & ack.capabilities & !client_caps;
assert_eq!(
rejected, 0,
"server must not set bits the client did not request"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn max_frame_size_enforced() {
let server = NativeTestServer::start().await;
let (mut stream, _ack) = do_handshake(server.addr, &HelloFrame::current())
.await
.expect("handshake");
let oversized_len = (MAX_FRAME_SIZE + 1).to_be_bytes();
stream
.write_all(&oversized_len)
.await
.expect("write oversized length prefix");
stream.flush().await.expect("flush");
let response_payload = tokio::time::timeout(Duration::from_secs(5), read_frame(&mut stream))
.await
.expect("server must respond within 5 seconds");
server.shutdown().await;
let payload = response_payload.expect("server must send an error response before closing");
let response: NativeResponse = zerompk::from_msgpack(&payload)
.or_else(|_| sonic_rs::from_slice(&payload))
.expect("response must be a valid NativeResponse");
assert_eq!(
response.status,
nodedb_types::protocol::opcodes::ResponseStatus::Error,
"response status must be Error for oversized frame"
);
let err = response.error.expect("error payload must be present");
assert!(
err.message.contains("frame") || err.message.contains("54000") || err.code == "54000",
"error must mention frame rejection, got code='{}' message='{}'",
err.code,
err.message
);
}
#[tokio::test]
async fn json_request_gets_json_response() {
let server = NativeTestServer::start().await;
let (mut stream, _ack) = do_handshake(server.addr, &HelloFrame::current())
.await
.expect("handshake");
let req = NativeRequest {
op: OpCode::Ping,
seq: 77,
fields: RequestFields::Text(TextFields::default()),
};
let json_bytes = sonic_rs::to_vec(&req).expect("json encode");
assert_eq!(json_bytes[0], b'{', "JSON must start with open brace");
write_frame(&mut stream, &json_bytes).await;
let response_payload = tokio::time::timeout(Duration::from_secs(5), read_frame(&mut stream))
.await
.expect("timeout")
.expect("response");
server.shutdown().await;
assert_eq!(
response_payload[0], b'{',
"JSON session must produce JSON response, got first byte 0x{:02X}",
response_payload[0]
);
let resp: NativeResponse = sonic_rs::from_slice(&response_payload).expect("json decode");
assert_eq!(resp.seq, 77, "seq must echo");
assert_eq!(
resp.status,
nodedb_types::protocol::opcodes::ResponseStatus::Ok,
"Ping must return Ok"
);
}
#[tokio::test]
async fn msgpack_request_gets_msgpack_response() {
let server = NativeTestServer::start().await;
let (mut stream, _ack) = do_handshake(server.addr, &HelloFrame::current())
.await
.expect("handshake");
let req = NativeRequest {
op: OpCode::Ping,
seq: 99,
fields: RequestFields::Text(TextFields::default()),
};
let mp_bytes = zerompk::to_msgpack_vec(&req).expect("msgpack encode");
assert_ne!(mp_bytes[0], b'{', "MsgPack must NOT start with open brace");
write_frame(&mut stream, &mp_bytes).await;
let response_payload = tokio::time::timeout(Duration::from_secs(5), read_frame(&mut stream))
.await
.expect("timeout")
.expect("response");
server.shutdown().await;
assert_ne!(
response_payload[0], b'{',
"MsgPack session must produce MsgPack response, got first byte 0x{:02X}",
response_payload[0]
);
let resp: NativeResponse = zerompk::from_msgpack(&response_payload).expect("msgpack decode");
assert_eq!(resp.seq, 99, "seq must echo");
assert_eq!(
resp.status,
nodedb_types::protocol::opcodes::ResponseStatus::Ok,
"Ping must return Ok"
);
}
#[tokio::test]
async fn mid_session_encoding_switch_rejected() {
let server = NativeTestServer::start().await;
let (mut stream, _ack) = do_handshake(server.addr, &HelloFrame::current())
.await
.expect("handshake");
let ping_json = NativeRequest {
op: OpCode::Ping,
seq: 1,
fields: RequestFields::Text(TextFields::default()),
};
let json_bytes = sonic_rs::to_vec(&ping_json).expect("json encode");
write_frame(&mut stream, &json_bytes).await;
let _first_resp = tokio::time::timeout(Duration::from_secs(5), read_frame(&mut stream))
.await
.expect("timeout")
.expect("first response");
let ping_mp = NativeRequest {
op: OpCode::Ping,
seq: 2,
fields: RequestFields::Text(TextFields::default()),
};
let mp_bytes = zerompk::to_msgpack_vec(&ping_mp).expect("msgpack encode");
write_frame(&mut stream, &mp_bytes).await;
let switch_resp = tokio::time::timeout(Duration::from_secs(5), read_frame(&mut stream))
.await
.expect("timeout")
.expect("switch response");
server.shutdown().await;
assert_eq!(
switch_resp[0], b'{',
"response must still be JSON after mid-session switch attempt"
);
let resp: NativeResponse = sonic_rs::from_slice(&switch_resp).expect("json decode");
assert_eq!(
resp.status,
nodedb_types::protocol::opcodes::ResponseStatus::Error,
"mid-session encoding switch must produce an Error response"
);
}