use nodedb::bridge::envelope::Status;
use nodedb::engine::graph::edge_store::Direction;
use nodedb_physical::physical_plan::{GraphOp, PhysicalPlan};
use crate::helpers::*;
#[test]
fn graph_cross_tenant_insert_does_not_contaminate_neighbors() {
let (mut core, mut tx, mut rx, _dir) = make_core();
send_ok_as_tenant(
&mut core,
&mut tx,
&mut rx,
TENANT_A,
PhysicalPlan::Graph(GraphOp::EdgePut {
collection: "social".into(),
src_id: "node_1".into(),
label: "FOLLOWS".into(),
dst_id: "node_2".into(),
properties: vec![],
src_surrogate: nodedb_types::Surrogate::ZERO,
dst_surrogate: nodedb_types::Surrogate::ZERO,
}),
);
let resp_baseline = send_raw_as_tenant(
&mut core,
&mut tx,
&mut rx,
TENANT_A,
PhysicalPlan::Graph(GraphOp::Neighbors {
node_id: "node_1".into(),
edge_label: Some("FOLLOWS".into()),
direction: Direction::Out,
rls_filters: Vec::new(),
}),
);
assert_eq!(resp_baseline.status, Status::Ok);
let json_baseline = payload_json(&resp_baseline.payload);
let baseline_count = serde_json::from_str::<serde_json::Value>(&json_baseline)
.unwrap_or(serde_json::Value::Array(vec![]))
.as_array()
.map(|a| a.len())
.unwrap_or(0);
assert!(
baseline_count >= 1,
"Tenant A should have at least 1 neighbor before B's inserts"
);
for i in 0..10u32 {
send_ok_as_tenant(
&mut core,
&mut tx,
&mut rx,
TENANT_B,
PhysicalPlan::Graph(GraphOp::EdgePut {
collection: "social".into(),
src_id: "node_1".into(),
label: "FOLLOWS".into(),
dst_id: format!("b_node_{i}"),
properties: vec![],
src_surrogate: nodedb_types::Surrogate::ZERO,
dst_surrogate: nodedb_types::Surrogate::ZERO,
}),
);
}
let resp_after = send_raw_as_tenant(
&mut core,
&mut tx,
&mut rx,
TENANT_A,
PhysicalPlan::Graph(GraphOp::Neighbors {
node_id: "node_1".into(),
edge_label: Some("FOLLOWS".into()),
direction: Direction::Out,
rls_filters: Vec::new(),
}),
);
assert_eq!(resp_after.status, Status::Ok);
let json_after = payload_json(&resp_after.payload);
let after_count = serde_json::from_str::<serde_json::Value>(&json_after)
.unwrap_or(serde_json::Value::Array(vec![]))
.as_array()
.map(|a| a.len())
.unwrap_or(0);
assert_eq!(
after_count, baseline_count,
"Tenant B's graph inserts must not contaminate Tenant A's neighbor results \
(baseline={baseline_count}, after={after_count}); json={json_after}"
);
assert!(
!json_after.contains("b_node_"),
"Tenant B's destination nodes must NOT appear in Tenant A's neighbor results; got: {json_after}"
);
}
#[test]
fn graph_cross_tenant_edge_delete_does_not_affect_owner() {
let (mut core, mut tx, mut rx, _dir) = make_core();
send_ok_as_tenant(
&mut core,
&mut tx,
&mut rx,
TENANT_A,
PhysicalPlan::Graph(GraphOp::EdgePut {
collection: "social".into(),
src_id: "alpha".into(),
label: "CONNECTED".into(),
dst_id: "beta".into(),
properties: vec![],
src_surrogate: nodedb_types::Surrogate::ZERO,
dst_surrogate: nodedb_types::Surrogate::ZERO,
}),
);
let resp_del = send_raw_as_tenant(
&mut core,
&mut tx,
&mut rx,
TENANT_B,
PhysicalPlan::Graph(GraphOp::EdgeDelete {
collection: "social".into(),
src_id: "alpha".into(),
label: "CONNECTED".into(),
dst_id: "beta".into(),
}),
);
let _ = resp_del;
let resp_a = send_raw_as_tenant(
&mut core,
&mut tx,
&mut rx,
TENANT_A,
PhysicalPlan::Graph(GraphOp::Neighbors {
node_id: "alpha".into(),
edge_label: Some("CONNECTED".into()),
direction: Direction::Out,
rls_filters: Vec::new(),
}),
);
assert_eq!(resp_a.status, Status::Ok);
let json_a = payload_json(&resp_a.payload);
assert!(
json_a.contains("beta"),
"Tenant A's edge must survive Tenant B's cross-tenant delete; got: {json_a}"
);
}