2026-04-24 13:37:30 -06:00
|
|
|
use alloc::{borrow::ToOwned, string::String, vec, vec::Vec};
|
|
|
|
|
|
|
|
|
|
use crate::protocol::tree::{
|
2026-04-25 11:27:29 -06:00
|
|
|
ChildRoute, DefaultRouteProvider, Endpoint, Ingress, LeafNode, LeafSpec, LocalEvent,
|
2026-04-24 13:37:30 -06:00
|
|
|
ProtocolEndpoint, RouteDecision, RouteProvider, TreeNode,
|
|
|
|
|
};
|
|
|
|
|
use crate::protocol::{
|
|
|
|
|
DataMessage, EndpointIntrospection, FaultMessage, PacketHeader, PacketType, ProtocolFault,
|
2026-04-24 14:25:35 -06:00
|
|
|
deserialize_archived_bytes, encode_packet,
|
2026-04-24 13:37:30 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn path(parts: &[&str]) -> Vec<String> {
|
|
|
|
|
parts.iter().map(|part| (*part).to_owned()).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn tree_node_paths_flatten_explicitly() {
|
|
|
|
|
let tree = TreeNode::Root {
|
|
|
|
|
children: vec![TreeNode::Endpoint {
|
|
|
|
|
segment: "branch".to_owned(),
|
|
|
|
|
leaves: vec![LeafNode {
|
2026-04-25 11:27:29 -06:00
|
|
|
name: "service".to_owned(),
|
|
|
|
|
procedures: vec!["example.service.v1.invoke".to_owned()],
|
2026-04-24 13:37:30 -06:00
|
|
|
}],
|
|
|
|
|
children: vec![TreeNode::Endpoint {
|
|
|
|
|
segment: "leaf".to_owned(),
|
|
|
|
|
leaves: Vec::new(),
|
|
|
|
|
children: Vec::new(),
|
|
|
|
|
}],
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
tree.paths(),
|
|
|
|
|
vec![
|
|
|
|
|
Vec::<String>::new(),
|
|
|
|
|
path(&["branch"]),
|
|
|
|
|
path(&["branch", "leaf"])
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn longest_prefix_routing_prefers_most_specific_child() {
|
|
|
|
|
let provider = DefaultRouteProvider;
|
|
|
|
|
let child_paths = vec![path(&["a"]), path(&["a", "b"]), path(&["x"])];
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider.route_destination(&Vec::new(), &child_paths, true, &path(&["a", "b", "c"])),
|
|
|
|
|
RouteDecision::Child(1)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider.route_destination(&path(&["a"]), &child_paths, true, &path(&["z"])),
|
|
|
|
|
RouteDecision::Parent
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn protocol_endpoint_introspection_returns_leaf_summary() {
|
|
|
|
|
let mut endpoint = ProtocolEndpoint::new(
|
|
|
|
|
path(&["root"]),
|
|
|
|
|
Some(Vec::new()),
|
2026-04-25 11:27:29 -06:00
|
|
|
vec![ChildRoute::registered(path(&["root", "child"]))],
|
2026-04-24 13:37:30 -06:00
|
|
|
vec![LeafSpec {
|
2026-04-25 11:27:29 -06:00
|
|
|
name: "service".to_owned(),
|
|
|
|
|
procedures: vec!["example.service.v1.invoke".to_owned()],
|
2026-04-24 13:37:30 -06:00
|
|
|
}],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let hook_id = endpoint.allocate_hook_id();
|
|
|
|
|
let frame = endpoint
|
|
|
|
|
.make_call(path(&["root"]), None, "", Some(hook_id), Vec::new())
|
|
|
|
|
.expect("introspection call should encode");
|
|
|
|
|
|
|
|
|
|
let outcome = endpoint
|
|
|
|
|
.receive(&Ingress::Local, frame)
|
|
|
|
|
.expect("endpoint should handle introspection");
|
|
|
|
|
|
2026-04-25 11:46:45 -06:00
|
|
|
assert!(outcome.forward.is_none());
|
2026-04-24 13:37:30 -06:00
|
|
|
|
2026-04-24 14:27:55 -06:00
|
|
|
let LocalEvent::Data {
|
|
|
|
|
header,
|
|
|
|
|
message: response,
|
2026-04-25 11:46:45 -06:00
|
|
|
} = outcome.event.as_ref().expect("expected local data event")
|
2026-04-24 14:27:55 -06:00
|
|
|
else {
|
2026-04-24 14:25:35 -06:00
|
|
|
panic!("expected local data event");
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(header.packet_type, PacketType::Data);
|
|
|
|
|
assert_eq!(header.dst_path, path(&["root"]));
|
2026-04-24 13:37:30 -06:00
|
|
|
let introspection = deserialize_archived_bytes::<
|
2026-04-24 14:25:35 -06:00
|
|
|
crate::protocol::introspection::ArchivedEndpointIntrospection,
|
2026-04-24 13:37:30 -06:00
|
|
|
EndpointIntrospection,
|
|
|
|
|
>(&response.data)
|
|
|
|
|
.expect("introspection payload should deserialize");
|
|
|
|
|
|
|
|
|
|
assert!(response.end_hook);
|
2026-04-25 11:27:29 -06:00
|
|
|
assert_eq!(introspection.sub_endpoints, vec!["child".to_owned()]);
|
2026-04-24 13:37:30 -06:00
|
|
|
assert_eq!(introspection.leaves.len(), 1);
|
2026-04-25 11:27:29 -06:00
|
|
|
assert_eq!(introspection.leaves[0].leaf_name, "service");
|
2026-04-24 13:37:30 -06:00
|
|
|
assert_eq!(
|
|
|
|
|
introspection.leaves[0].procedures,
|
2026-04-25 11:27:29 -06:00
|
|
|
vec!["example.service.v1.invoke".to_owned()]
|
2026-04-24 13:37:30 -06:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn invalid_hook_peer_emits_local_fault_event() {
|
|
|
|
|
let mut endpoint = ProtocolEndpoint::new(path(&["client"]), None, Vec::new(), Vec::new());
|
|
|
|
|
let hook_id = endpoint.allocate_hook_id();
|
|
|
|
|
|
|
|
|
|
endpoint
|
|
|
|
|
.make_call(
|
|
|
|
|
path(&["server"]),
|
|
|
|
|
None,
|
2026-04-25 11:27:29 -06:00
|
|
|
"example.service.v1.invoke",
|
2026-04-24 13:37:30 -06:00
|
|
|
Some(hook_id),
|
|
|
|
|
vec![1, 2, 3],
|
|
|
|
|
)
|
|
|
|
|
.expect("call should establish an active hook");
|
|
|
|
|
|
|
|
|
|
let frame = encode_packet(
|
|
|
|
|
&PacketHeader {
|
|
|
|
|
packet_type: PacketType::Data,
|
|
|
|
|
src_path: path(&["client"]),
|
|
|
|
|
dst_path: path(&["client"]),
|
|
|
|
|
dst_leaf: None,
|
|
|
|
|
hook_id: Some(hook_id),
|
|
|
|
|
},
|
|
|
|
|
&DataMessage {
|
2026-04-25 11:27:29 -06:00
|
|
|
procedure_id: "example.service.v1.invoke".to_owned(),
|
2026-04-24 13:37:30 -06:00
|
|
|
data: vec![9],
|
|
|
|
|
end_hook: false,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.expect("data frame should encode");
|
|
|
|
|
|
|
|
|
|
let outcome = endpoint
|
|
|
|
|
.receive(&Ingress::Local, frame)
|
|
|
|
|
.expect("invalid peer should be handled");
|
|
|
|
|
|
2026-04-25 11:46:45 -06:00
|
|
|
assert!(outcome.forward.is_none());
|
2026-04-24 13:37:30 -06:00
|
|
|
assert!(!outcome.dropped);
|
|
|
|
|
|
2026-04-25 11:46:45 -06:00
|
|
|
match outcome.event.as_ref().expect("expected event") {
|
2026-04-24 13:37:30 -06:00
|
|
|
LocalEvent::Fault { header, message } => {
|
|
|
|
|
assert_eq!(header.packet_type, PacketType::Fault);
|
|
|
|
|
assert_eq!(header.hook_id, Some(hook_id));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
message,
|
|
|
|
|
&FaultMessage {
|
2026-04-24 14:25:35 -06:00
|
|
|
fault: ProtocolFault::INVALID_HOOK_PEER,
|
2026-04-24 13:37:30 -06:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
other => panic!("expected fault event, got {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-25 12:37:54 -06:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn hook_closes_only_after_both_sides_end() {
|
|
|
|
|
let mut endpoint = ProtocolEndpoint::new(
|
|
|
|
|
Vec::new(),
|
|
|
|
|
None,
|
|
|
|
|
vec![ChildRoute::registered(path(&["server"]))],
|
|
|
|
|
Vec::new(),
|
|
|
|
|
);
|
|
|
|
|
let hook_id = endpoint.allocate_hook_id();
|
|
|
|
|
|
|
|
|
|
endpoint
|
|
|
|
|
.make_call(
|
|
|
|
|
path(&["server"]),
|
|
|
|
|
None,
|
|
|
|
|
"example.service.v1.invoke",
|
|
|
|
|
Some(hook_id),
|
|
|
|
|
vec![1],
|
|
|
|
|
)
|
|
|
|
|
.expect("call should establish an active hook");
|
|
|
|
|
|
|
|
|
|
let host_key = crate::protocol::tree::HookKey::new(Vec::new(), hook_id);
|
|
|
|
|
assert!(endpoint.hooks.active(&host_key).is_some());
|
|
|
|
|
|
|
|
|
|
endpoint
|
|
|
|
|
.send_data(
|
|
|
|
|
path(&["server"]),
|
|
|
|
|
hook_id,
|
|
|
|
|
"example.service.v1.invoke",
|
|
|
|
|
vec![2],
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.expect("local end should succeed");
|
|
|
|
|
assert!(endpoint.hooks.active(&host_key).is_some());
|
|
|
|
|
|
|
|
|
|
let frame = encode_packet(
|
|
|
|
|
&PacketHeader {
|
|
|
|
|
packet_type: PacketType::Data,
|
|
|
|
|
src_path: path(&["server"]),
|
|
|
|
|
dst_path: Vec::new(),
|
|
|
|
|
dst_leaf: None,
|
|
|
|
|
hook_id: Some(hook_id),
|
|
|
|
|
},
|
|
|
|
|
&DataMessage {
|
|
|
|
|
procedure_id: "example.service.v1.invoke".to_owned(),
|
|
|
|
|
data: vec![3],
|
|
|
|
|
end_hook: true,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.expect("peer final data should encode");
|
|
|
|
|
|
|
|
|
|
endpoint
|
|
|
|
|
.receive(&Ingress::Child(path(&["server"])), frame)
|
|
|
|
|
.expect("peer final data should be handled");
|
|
|
|
|
assert!(endpoint.hooks.active(&host_key).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pending_hook_fault_is_delivered_before_activation() {
|
|
|
|
|
let mut endpoint = ProtocolEndpoint::new(path(&["server"]), None, Vec::new(), Vec::new());
|
|
|
|
|
let header = PacketHeader {
|
|
|
|
|
packet_type: PacketType::Call,
|
|
|
|
|
src_path: path(&["client"]),
|
|
|
|
|
dst_path: path(&["server"]),
|
|
|
|
|
dst_leaf: None,
|
|
|
|
|
hook_id: None,
|
|
|
|
|
};
|
|
|
|
|
let call = crate::protocol::CallMessage {
|
|
|
|
|
procedure_id: crate::protocol::INTROSPECTION_PROCEDURE_ID.to_owned(),
|
|
|
|
|
data: Vec::new(),
|
|
|
|
|
response_hook: Some(crate::protocol::HookTarget {
|
|
|
|
|
hook_id: 11,
|
|
|
|
|
return_path: path(&["client"]),
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
endpoint
|
|
|
|
|
.hooks
|
|
|
|
|
.insert_pending(crate::protocol::tree::PendingHook {
|
|
|
|
|
return_path: path(&["client"]),
|
|
|
|
|
hook_id: 11,
|
|
|
|
|
caller_src_path: path(&["client"]),
|
|
|
|
|
procedure_id: call.procedure_id.clone(),
|
|
|
|
|
dst_leaf: None,
|
|
|
|
|
})
|
|
|
|
|
.expect("pending hook should insert");
|
|
|
|
|
|
|
|
|
|
let outcome = endpoint
|
2026-04-25 12:41:10 -06:00
|
|
|
.handle_introspection(
|
|
|
|
|
&header,
|
|
|
|
|
Some(crate::protocol::tree::HookKey::new(path(&["client"]), 11)),
|
|
|
|
|
)
|
2026-04-25 12:37:54 -06:00
|
|
|
.expect("introspection should handle pending hook");
|
|
|
|
|
|
|
|
|
|
assert!(outcome.forward.is_some() || outcome.event.is_some());
|
|
|
|
|
}
|