Loading crates/integration-testkit/src/visitor/enforcement.rs +122 −2 Original line number Diff line number Diff line Loading @@ -254,6 +254,7 @@ mod tests { ); let reqs = input.requirements(); assert!(reqs.contains(&Requirement::NodeIds)); assert_eq!(reqs.len(), 1); } #[test] Loading @@ -272,6 +273,7 @@ mod tests { !reqs.contains(&Requirement::NodeIds), "path_finding node_ids are endpoints, not result filters" ); assert_eq!(reqs.len(), 1); } #[test] Loading @@ -284,6 +286,7 @@ mod tests { ); let reqs = input.requirements(); assert!(reqs.contains(&Requirement::Aggregation)); assert_eq!(reqs.len(), 1); } #[test] Loading Loading @@ -376,6 +379,7 @@ mod tests { let reqs = input.requirements(); assert!(reqs.contains(&Requirement::Neighbors)); assert!(reqs.contains(&Requirement::NodeIds)); assert_eq!(reqs.len(), 2); } #[test] Loading @@ -390,6 +394,7 @@ mod tests { let reqs = input.requirements(); assert!(reqs.contains(&Requirement::AggregationSort)); assert!(reqs.contains(&Requirement::Aggregation)); assert_eq!(reqs.len(), 2); } #[test] Loading @@ -400,7 +405,9 @@ mod tests { "range": {"start": 0, "end": 5}, "limit": 10}"#, ); assert!(input.requirements().contains(&Requirement::Range)); let reqs = input.requirements(); assert!(reqs.contains(&Requirement::Range)); assert_eq!(reqs.len(), 1); } // ── Assertion enforcement ──────────────────────────────────────── Loading Loading @@ -445,7 +452,9 @@ mod tests { ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_filter("User", "username", |n| n.prop_str("username").is_some()); view.assert_filter("User", "state", |n| n.prop_str("username").is_some()); view.assert_filter("User", "state", |n| { matches!(n.prop_str("username"), Some("alice" | "bob")) }); } #[test] Loading Loading @@ -639,6 +648,54 @@ mod tests { view.assert_node_count(2); } #[test] fn for_query_node_ids_satisfied_by_assert_node_ids() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2]}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_node_ids("User", &[1, 2]); } #[test] fn for_query_neighbors_satisfied_by_assert_edge_set() { let input = parse_test_input( r#"{"query_type": "neighbors", "node": {"id": "u", "entity": "User", "node_ids": [1]}, "neighbors": {"node": "u", "direction": "outgoing"}}"#, ); let view = ResponseView::for_query(&input, sample_neighbors_response()); view.assert_edge_set("MEMBER_OF", &[(1, 100), (1, 101)]); let _ = view.node_ids("User").into_inner(); } #[test] fn for_query_neighbors_satisfied_by_assert_edge_count() { let input = parse_test_input( r#"{"query_type": "neighbors", "node": {"id": "u", "entity": "User", "node_ids": [1]}, "neighbors": {"node": "u", "direction": "outgoing"}}"#, ); let view = ResponseView::for_query(&input, sample_neighbors_response()); view.assert_edge_count("MEMBER_OF", 2); let _ = view.node_ids("User").into_inner(); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_node_ids_not_satisfied_by_assert_node_count() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2]}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_node_count(2); drop(view); } // ── Panic on unsatisfied ───────────────────────────────────────── #[test] Loading Loading @@ -718,6 +775,69 @@ mod tests { drop(view); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_panics_on_unsatisfied_path_finding() { let input = parse_test_input( r#"{"query_type": "path_finding", "nodes": [{"id": "s", "entity": "User", "node_ids": [1]}, {"id": "e", "entity": "Project", "node_ids": [1000]}], "path": {"type": "shortest", "from": "s", "to": "e", "max_depth": 3}}"#, ); let resp = GraphResponse { query_type: "path_finding".to_string(), nodes: vec![make_node("User", 1, &[]), make_node("Project", 1000, &[])], edges: vec![make_path_edge("User", 1, "Project", 1000, "CONTAINS", 0, 0)], }; let view = ResponseView::for_query(&input, resp); drop(view); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_panics_on_unsatisfied_aggregation_sort() { let input = parse_test_input( r#"{"query_type": "aggregation", "nodes": [{"id": "u", "entity": "User"}], "aggregations": [{"function": "count", "target": "u", "alias": "c"}], "aggregation_sort": {"agg_index": 0, "direction": "DESC"}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_aggregation_response()); view.assert_node("User", 1, |n| n.prop_str("username") == Some("alice")); drop(view); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_panics_on_unsatisfied_node_ids() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2]}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); drop(view); } #[test] fn for_query_combined_features_requires_all() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2], "filters": {"username": {"op": "in", "value": ["alice", "bob"]}}}, "order_by": {"node": "u", "property": "id"}, "range": {"start": 0, "end": 5}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_node_order("User", &[1, 2]); view.assert_node_count(2); view.assert_filter("User", "username", |n| { matches!(n.prop_str("username"), Some("alice" | "bob")) }); } // ── Skip + new() ───────────────────────────────────────────────── #[test] Loading crates/integration-testkit/src/visitor/mod.rs +52 −104 Original line number Diff line number Diff line Loading @@ -345,20 +345,6 @@ impl ResponseView { edges } // ── Visitor ────────────────────────────────────────────────────── pub fn visit_nodes(&self, mut f: impl FnMut(&GraphNode)) { for node in &self.response.nodes { f(node); } } pub fn visit_edges(&self, mut f: impl FnMut(&GraphEdge)) { for edge in &self.response.edges { f(edge); } } // ── Assertions ─────────────────────────────────────────────────── pub fn assert_node_exists(&self, entity_type: &str, id: i64) { Loading Loading @@ -493,6 +479,58 @@ impl ResponseView { } } /// Assert that the IDs of nodes with `entity_type` match `expected` exactly (unordered). /// Satisfies [`Requirement::NodeIds`]. pub fn assert_node_ids(&self, entity_type: &str, expected: &[i64]) { self.tracker.satisfy(Requirement::NodeIds); let actual: HashSet<i64> = self .response .nodes .iter() .filter(|n| n.entity_type == entity_type) .map(|n| n.id) .collect(); let expected_set: HashSet<i64> = expected.iter().copied().collect(); assert_eq!(actual, expected_set, "{entity_type} node IDs mismatch"); } /// Assert the exact set of `(from_id, to_id)` pairs for edges of `edge_type`. /// Satisfies [`Requirement::Relationship`] and [`Requirement::Neighbors`]. pub fn assert_edge_set(&self, edge_type: &str, expected: &[(i64, i64)]) { self.tracker.satisfy(Requirement::Relationship { edge_type: edge_type.to_string(), }); self.tracker.satisfy(Requirement::Neighbors); let actual: HashSet<(i64, i64)> = self .response .edges .iter() .filter(|e| e.edge_type == edge_type) .map(|e| (e.from_id, e.to_id)) .collect(); let expected_set: HashSet<(i64, i64)> = expected.iter().copied().collect(); assert_eq!(actual, expected_set, "{edge_type} edge set mismatch"); } /// Assert the number of edges with `edge_type`. /// Satisfies [`Requirement::Relationship`] and [`Requirement::Neighbors`]. pub fn assert_edge_count(&self, edge_type: &str, expected: usize) { self.tracker.satisfy(Requirement::Relationship { edge_type: edge_type.to_string(), }); self.tracker.satisfy(Requirement::Neighbors); let actual = self .response .edges .iter() .filter(|e| e.edge_type == edge_type) .count(); assert_eq!( actual, expected, "expected {expected} {edge_type} edges, got {actual}" ); } /// Assert that nodes of the given type appear in exactly this ID order. /// Satisfies [`Requirement::OrderBy`], [`Requirement::NodeIds`], and /// [`Requirement::AggregationSort`]. Loading Loading @@ -617,38 +655,6 @@ impl NodeExt for GraphNode { } } // ───────────────────────────────────────────────────────────────────────────── // EdgeExt // ───────────────────────────────────────────────────────────────────────────── pub trait EdgeExt { fn connects(&self, from: &str, from_id: i64, to: &str, to_id: i64) -> bool; } impl EdgeExt for GraphEdge { fn connects(&self, from: &str, from_id: i64, to: &str, to_id: i64) -> bool { self.from == from && self.from_id == from_id && self.to == to && self.to_id == to_id } } // ───────────────────────────────────────────────────────────────────────────── // ResponseVisitor trait // ───────────────────────────────────────────────────────────────────────────── pub trait ResponseVisitor { fn visit_node(&mut self, _node: &GraphNode) {} fn visit_edge(&mut self, _edge: &GraphEdge) {} } pub fn walk_response(response: &ResponseView, visitor: &mut impl ResponseVisitor) { for node in &response.response.nodes { visitor.visit_node(node); } for edge in &response.response.edges { visitor.visit_edge(edge); } } #[cfg(test)] pub(crate) mod tests { use super::*; Loading Loading @@ -936,24 +942,6 @@ pub(crate) mod tests { assert!(view.path(99).is_empty()); } // ── Visitor helpers ────────────────────────────────────────────── #[test] fn visit_nodes_visits_all() { let view = ResponseView::new(sample_response()); let mut count = 0; view.visit_nodes(|_| count += 1); assert_eq!(count, 4); } #[test] fn visit_edges_visits_all() { let view = ResponseView::new(sample_response()); let mut count = 0; view.visit_edges(|_| count += 1); assert_eq!(count, 3); } // ── Assertions ─────────────────────────────────────────────────── #[test] Loading Loading @@ -1149,46 +1137,6 @@ pub(crate) mod tests { node.assert_prop("tags", &json!(["a", "b"])); } // ── EdgeExt ────────────────────────────────────────────────────── #[test] fn connects_returns_true_for_matching_endpoints() { let edge = make_edge("User", 1, "Group", 100, "MEMBER_OF"); assert!(edge.connects("User", 1, "Group", 100)); } #[test] fn connects_returns_false_for_wrong_endpoints() { let edge = make_edge("User", 1, "Group", 100, "MEMBER_OF"); assert!(!edge.connects("User", 1, "Group", 999)); assert!(!edge.connects("User", 2, "Group", 100)); assert!(!edge.connects("Project", 1, "Group", 100)); } // ── walk_response ──────────────────────────────────────────────── #[test] fn walk_response_visits_all_nodes_and_edges() { struct Counter { nodes: usize, edges: usize, } impl ResponseVisitor for Counter { fn visit_node(&mut self, _: &GraphNode) { self.nodes += 1; } fn visit_edge(&mut self, _: &GraphEdge) { self.edges += 1; } } let view = ResponseView::new(sample_response()); let mut counter = Counter { nodes: 0, edges: 0 }; walk_response(&view, &mut counter); assert_eq!(counter.nodes, 4); assert_eq!(counter.edges, 3); } // ── Empty response ─────────────────────────────────────────────── #[test] Loading crates/integration-tests/tests/server/data_correctness.rs +21 −43 Original line number Diff line number Diff line Loading @@ -309,8 +309,7 @@ async fn search_filter_in_returns_matching_rows(ctx: &TestContext) { ) .await; let ids = resp.node_ids("Project"); assert_eq!(ids, HashSet::from([1000, 1002, 1004])); resp.assert_node_ids("Project", &[1000, 1002, 1004]); resp.assert_filter("Project", "visibility_level", |n| { let vis = n.prop_str("visibility_level").unwrap_or(""); Loading Loading @@ -354,7 +353,7 @@ async fn search_node_ids_returns_only_specified(ctx: &TestContext) { ) .await; assert_eq!(resp.node_ids("Group"), HashSet::from([100, 102])); resp.assert_node_ids("Group", &[100, 102]); resp.find_node("Group", 100) .unwrap() .assert_str("name", "Public Group"); Loading Loading @@ -385,7 +384,9 @@ async fn traversal_user_group_returns_correct_pairs_and_edges(ctx: &TestContext) ) .await; let expected_edges: HashSet<(i64, i64)> = HashSet::from([ resp.assert_edge_set( "MEMBER_OF", &[ (1, 100), (1, 102), (2, 100), Loading @@ -393,16 +394,7 @@ async fn traversal_user_group_returns_correct_pairs_and_edges(ctx: &TestContext) (4, 101), (4, 102), (5, 101), ]); let actual_edges: HashSet<(i64, i64)> = resp .edges_of_type("MEMBER_OF") .iter() .map(|e| (e.from_id, e.to_id)) .collect(); assert_eq!( actual_edges, expected_edges, "edges must match seeded MEMBER_OF relationships" ], ); resp.assert_referential_integrity(); Loading @@ -412,12 +404,6 @@ async fn traversal_user_group_returns_correct_pairs_and_edges(ctx: &TestContext) resp.assert_node("Group", 101, |n| { n.prop_str("name") == Some("Private Group") }); resp.visit_edges(|e| { assert_eq!(e.edge_type, "MEMBER_OF"); assert_eq!(e.from, "User"); assert_eq!(e.to, "Group"); }); } async fn traversal_three_hop_returns_all_user_group_project_paths(ctx: &TestContext) { Loading Loading @@ -522,8 +508,8 @@ async fn traversal_redaction_removes_unauthorized_data(ctx: &TestContext) { ) .await; assert_eq!(resp.node_ids("User"), HashSet::from([1])); assert_eq!(resp.node_ids("Group"), HashSet::from([100])); resp.assert_node_ids("User", &[1]); resp.assert_node_ids("Group", &[100]); resp.assert_node_absent("User", 2); resp.assert_node_absent("Group", 102); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); Loading Loading @@ -742,8 +728,7 @@ async fn neighbors_outgoing_returns_correct_targets(ctx: &TestContext) { resp.assert_referential_integrity(); let neighbor_ids = resp.node_ids("Group"); assert_eq!(neighbor_ids, HashSet::from([100, 102])); resp.assert_node_ids("Group", &[100, 102]); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); resp.assert_edge_exists("User", 1, "Group", 102, "MEMBER_OF"); Loading @@ -767,8 +752,7 @@ async fn neighbors_incoming_returns_correct_sources(ctx: &TestContext) { ) .await; let user_ids = resp.node_ids("User"); assert_eq!(user_ids, HashSet::from([1, 2])); resp.assert_node_ids("User", &[1, 2]); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); resp.assert_edge_exists("User", 2, "Group", 100, "MEMBER_OF"); Loading @@ -787,11 +771,8 @@ async fn neighbors_rel_types_filter_works(ctx: &TestContext) { ) .await; let project_ids = resp.node_ids("Project"); assert_eq!(project_ids, HashSet::from([1000, 1002])); let edges = resp.edges_of_type("CONTAINS"); assert_eq!(edges.len(), 2, "should have exactly 2 CONTAINS edges"); resp.assert_node_ids("Project", &[1000, 1002]); resp.assert_edge_count("CONTAINS", 2); } async fn neighbors_both_direction_returns_all_connected(ctx: &TestContext) { Loading @@ -807,11 +788,8 @@ async fn neighbors_both_direction_returns_all_connected(ctx: &TestContext) { ) .await; let user_ids = resp.node_ids("User"); let project_ids = resp.node_ids("Project"); assert_eq!(user_ids, HashSet::from([1, 2])); assert_eq!(project_ids, HashSet::from([1000, 1002])); resp.assert_node_ids("User", &[1, 2]); resp.assert_node_ids("Project", &[1000, 1002]); resp.assert_referential_integrity(); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); Loading Loading
crates/integration-testkit/src/visitor/enforcement.rs +122 −2 Original line number Diff line number Diff line Loading @@ -254,6 +254,7 @@ mod tests { ); let reqs = input.requirements(); assert!(reqs.contains(&Requirement::NodeIds)); assert_eq!(reqs.len(), 1); } #[test] Loading @@ -272,6 +273,7 @@ mod tests { !reqs.contains(&Requirement::NodeIds), "path_finding node_ids are endpoints, not result filters" ); assert_eq!(reqs.len(), 1); } #[test] Loading @@ -284,6 +286,7 @@ mod tests { ); let reqs = input.requirements(); assert!(reqs.contains(&Requirement::Aggregation)); assert_eq!(reqs.len(), 1); } #[test] Loading Loading @@ -376,6 +379,7 @@ mod tests { let reqs = input.requirements(); assert!(reqs.contains(&Requirement::Neighbors)); assert!(reqs.contains(&Requirement::NodeIds)); assert_eq!(reqs.len(), 2); } #[test] Loading @@ -390,6 +394,7 @@ mod tests { let reqs = input.requirements(); assert!(reqs.contains(&Requirement::AggregationSort)); assert!(reqs.contains(&Requirement::Aggregation)); assert_eq!(reqs.len(), 2); } #[test] Loading @@ -400,7 +405,9 @@ mod tests { "range": {"start": 0, "end": 5}, "limit": 10}"#, ); assert!(input.requirements().contains(&Requirement::Range)); let reqs = input.requirements(); assert!(reqs.contains(&Requirement::Range)); assert_eq!(reqs.len(), 1); } // ── Assertion enforcement ──────────────────────────────────────── Loading Loading @@ -445,7 +452,9 @@ mod tests { ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_filter("User", "username", |n| n.prop_str("username").is_some()); view.assert_filter("User", "state", |n| n.prop_str("username").is_some()); view.assert_filter("User", "state", |n| { matches!(n.prop_str("username"), Some("alice" | "bob")) }); } #[test] Loading Loading @@ -639,6 +648,54 @@ mod tests { view.assert_node_count(2); } #[test] fn for_query_node_ids_satisfied_by_assert_node_ids() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2]}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_node_ids("User", &[1, 2]); } #[test] fn for_query_neighbors_satisfied_by_assert_edge_set() { let input = parse_test_input( r#"{"query_type": "neighbors", "node": {"id": "u", "entity": "User", "node_ids": [1]}, "neighbors": {"node": "u", "direction": "outgoing"}}"#, ); let view = ResponseView::for_query(&input, sample_neighbors_response()); view.assert_edge_set("MEMBER_OF", &[(1, 100), (1, 101)]); let _ = view.node_ids("User").into_inner(); } #[test] fn for_query_neighbors_satisfied_by_assert_edge_count() { let input = parse_test_input( r#"{"query_type": "neighbors", "node": {"id": "u", "entity": "User", "node_ids": [1]}, "neighbors": {"node": "u", "direction": "outgoing"}}"#, ); let view = ResponseView::for_query(&input, sample_neighbors_response()); view.assert_edge_count("MEMBER_OF", 2); let _ = view.node_ids("User").into_inner(); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_node_ids_not_satisfied_by_assert_node_count() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2]}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_node_count(2); drop(view); } // ── Panic on unsatisfied ───────────────────────────────────────── #[test] Loading Loading @@ -718,6 +775,69 @@ mod tests { drop(view); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_panics_on_unsatisfied_path_finding() { let input = parse_test_input( r#"{"query_type": "path_finding", "nodes": [{"id": "s", "entity": "User", "node_ids": [1]}, {"id": "e", "entity": "Project", "node_ids": [1000]}], "path": {"type": "shortest", "from": "s", "to": "e", "max_depth": 3}}"#, ); let resp = GraphResponse { query_type: "path_finding".to_string(), nodes: vec![make_node("User", 1, &[]), make_node("Project", 1000, &[])], edges: vec![make_path_edge("User", 1, "Project", 1000, "CONTAINS", 0, 0)], }; let view = ResponseView::for_query(&input, resp); drop(view); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_panics_on_unsatisfied_aggregation_sort() { let input = parse_test_input( r#"{"query_type": "aggregation", "nodes": [{"id": "u", "entity": "User"}], "aggregations": [{"function": "count", "target": "u", "alias": "c"}], "aggregation_sort": {"agg_index": 0, "direction": "DESC"}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_aggregation_response()); view.assert_node("User", 1, |n| n.prop_str("username") == Some("alice")); drop(view); } #[test] #[should_panic(expected = "unsatisfied assertion requirements")] fn for_query_panics_on_unsatisfied_node_ids() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2]}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); drop(view); } #[test] fn for_query_combined_features_requires_all() { let input = parse_test_input( r#"{"query_type": "search", "node": {"id": "u", "entity": "User", "node_ids": [1, 2], "filters": {"username": {"op": "in", "value": ["alice", "bob"]}}}, "order_by": {"node": "u", "property": "id"}, "range": {"start": 0, "end": 5}, "limit": 10}"#, ); let view = ResponseView::for_query(&input, sample_search_response()); view.assert_node_order("User", &[1, 2]); view.assert_node_count(2); view.assert_filter("User", "username", |n| { matches!(n.prop_str("username"), Some("alice" | "bob")) }); } // ── Skip + new() ───────────────────────────────────────────────── #[test] Loading
crates/integration-testkit/src/visitor/mod.rs +52 −104 Original line number Diff line number Diff line Loading @@ -345,20 +345,6 @@ impl ResponseView { edges } // ── Visitor ────────────────────────────────────────────────────── pub fn visit_nodes(&self, mut f: impl FnMut(&GraphNode)) { for node in &self.response.nodes { f(node); } } pub fn visit_edges(&self, mut f: impl FnMut(&GraphEdge)) { for edge in &self.response.edges { f(edge); } } // ── Assertions ─────────────────────────────────────────────────── pub fn assert_node_exists(&self, entity_type: &str, id: i64) { Loading Loading @@ -493,6 +479,58 @@ impl ResponseView { } } /// Assert that the IDs of nodes with `entity_type` match `expected` exactly (unordered). /// Satisfies [`Requirement::NodeIds`]. pub fn assert_node_ids(&self, entity_type: &str, expected: &[i64]) { self.tracker.satisfy(Requirement::NodeIds); let actual: HashSet<i64> = self .response .nodes .iter() .filter(|n| n.entity_type == entity_type) .map(|n| n.id) .collect(); let expected_set: HashSet<i64> = expected.iter().copied().collect(); assert_eq!(actual, expected_set, "{entity_type} node IDs mismatch"); } /// Assert the exact set of `(from_id, to_id)` pairs for edges of `edge_type`. /// Satisfies [`Requirement::Relationship`] and [`Requirement::Neighbors`]. pub fn assert_edge_set(&self, edge_type: &str, expected: &[(i64, i64)]) { self.tracker.satisfy(Requirement::Relationship { edge_type: edge_type.to_string(), }); self.tracker.satisfy(Requirement::Neighbors); let actual: HashSet<(i64, i64)> = self .response .edges .iter() .filter(|e| e.edge_type == edge_type) .map(|e| (e.from_id, e.to_id)) .collect(); let expected_set: HashSet<(i64, i64)> = expected.iter().copied().collect(); assert_eq!(actual, expected_set, "{edge_type} edge set mismatch"); } /// Assert the number of edges with `edge_type`. /// Satisfies [`Requirement::Relationship`] and [`Requirement::Neighbors`]. pub fn assert_edge_count(&self, edge_type: &str, expected: usize) { self.tracker.satisfy(Requirement::Relationship { edge_type: edge_type.to_string(), }); self.tracker.satisfy(Requirement::Neighbors); let actual = self .response .edges .iter() .filter(|e| e.edge_type == edge_type) .count(); assert_eq!( actual, expected, "expected {expected} {edge_type} edges, got {actual}" ); } /// Assert that nodes of the given type appear in exactly this ID order. /// Satisfies [`Requirement::OrderBy`], [`Requirement::NodeIds`], and /// [`Requirement::AggregationSort`]. Loading Loading @@ -617,38 +655,6 @@ impl NodeExt for GraphNode { } } // ───────────────────────────────────────────────────────────────────────────── // EdgeExt // ───────────────────────────────────────────────────────────────────────────── pub trait EdgeExt { fn connects(&self, from: &str, from_id: i64, to: &str, to_id: i64) -> bool; } impl EdgeExt for GraphEdge { fn connects(&self, from: &str, from_id: i64, to: &str, to_id: i64) -> bool { self.from == from && self.from_id == from_id && self.to == to && self.to_id == to_id } } // ───────────────────────────────────────────────────────────────────────────── // ResponseVisitor trait // ───────────────────────────────────────────────────────────────────────────── pub trait ResponseVisitor { fn visit_node(&mut self, _node: &GraphNode) {} fn visit_edge(&mut self, _edge: &GraphEdge) {} } pub fn walk_response(response: &ResponseView, visitor: &mut impl ResponseVisitor) { for node in &response.response.nodes { visitor.visit_node(node); } for edge in &response.response.edges { visitor.visit_edge(edge); } } #[cfg(test)] pub(crate) mod tests { use super::*; Loading Loading @@ -936,24 +942,6 @@ pub(crate) mod tests { assert!(view.path(99).is_empty()); } // ── Visitor helpers ────────────────────────────────────────────── #[test] fn visit_nodes_visits_all() { let view = ResponseView::new(sample_response()); let mut count = 0; view.visit_nodes(|_| count += 1); assert_eq!(count, 4); } #[test] fn visit_edges_visits_all() { let view = ResponseView::new(sample_response()); let mut count = 0; view.visit_edges(|_| count += 1); assert_eq!(count, 3); } // ── Assertions ─────────────────────────────────────────────────── #[test] Loading Loading @@ -1149,46 +1137,6 @@ pub(crate) mod tests { node.assert_prop("tags", &json!(["a", "b"])); } // ── EdgeExt ────────────────────────────────────────────────────── #[test] fn connects_returns_true_for_matching_endpoints() { let edge = make_edge("User", 1, "Group", 100, "MEMBER_OF"); assert!(edge.connects("User", 1, "Group", 100)); } #[test] fn connects_returns_false_for_wrong_endpoints() { let edge = make_edge("User", 1, "Group", 100, "MEMBER_OF"); assert!(!edge.connects("User", 1, "Group", 999)); assert!(!edge.connects("User", 2, "Group", 100)); assert!(!edge.connects("Project", 1, "Group", 100)); } // ── walk_response ──────────────────────────────────────────────── #[test] fn walk_response_visits_all_nodes_and_edges() { struct Counter { nodes: usize, edges: usize, } impl ResponseVisitor for Counter { fn visit_node(&mut self, _: &GraphNode) { self.nodes += 1; } fn visit_edge(&mut self, _: &GraphEdge) { self.edges += 1; } } let view = ResponseView::new(sample_response()); let mut counter = Counter { nodes: 0, edges: 0 }; walk_response(&view, &mut counter); assert_eq!(counter.nodes, 4); assert_eq!(counter.edges, 3); } // ── Empty response ─────────────────────────────────────────────── #[test] Loading
crates/integration-tests/tests/server/data_correctness.rs +21 −43 Original line number Diff line number Diff line Loading @@ -309,8 +309,7 @@ async fn search_filter_in_returns_matching_rows(ctx: &TestContext) { ) .await; let ids = resp.node_ids("Project"); assert_eq!(ids, HashSet::from([1000, 1002, 1004])); resp.assert_node_ids("Project", &[1000, 1002, 1004]); resp.assert_filter("Project", "visibility_level", |n| { let vis = n.prop_str("visibility_level").unwrap_or(""); Loading Loading @@ -354,7 +353,7 @@ async fn search_node_ids_returns_only_specified(ctx: &TestContext) { ) .await; assert_eq!(resp.node_ids("Group"), HashSet::from([100, 102])); resp.assert_node_ids("Group", &[100, 102]); resp.find_node("Group", 100) .unwrap() .assert_str("name", "Public Group"); Loading Loading @@ -385,7 +384,9 @@ async fn traversal_user_group_returns_correct_pairs_and_edges(ctx: &TestContext) ) .await; let expected_edges: HashSet<(i64, i64)> = HashSet::from([ resp.assert_edge_set( "MEMBER_OF", &[ (1, 100), (1, 102), (2, 100), Loading @@ -393,16 +394,7 @@ async fn traversal_user_group_returns_correct_pairs_and_edges(ctx: &TestContext) (4, 101), (4, 102), (5, 101), ]); let actual_edges: HashSet<(i64, i64)> = resp .edges_of_type("MEMBER_OF") .iter() .map(|e| (e.from_id, e.to_id)) .collect(); assert_eq!( actual_edges, expected_edges, "edges must match seeded MEMBER_OF relationships" ], ); resp.assert_referential_integrity(); Loading @@ -412,12 +404,6 @@ async fn traversal_user_group_returns_correct_pairs_and_edges(ctx: &TestContext) resp.assert_node("Group", 101, |n| { n.prop_str("name") == Some("Private Group") }); resp.visit_edges(|e| { assert_eq!(e.edge_type, "MEMBER_OF"); assert_eq!(e.from, "User"); assert_eq!(e.to, "Group"); }); } async fn traversal_three_hop_returns_all_user_group_project_paths(ctx: &TestContext) { Loading Loading @@ -522,8 +508,8 @@ async fn traversal_redaction_removes_unauthorized_data(ctx: &TestContext) { ) .await; assert_eq!(resp.node_ids("User"), HashSet::from([1])); assert_eq!(resp.node_ids("Group"), HashSet::from([100])); resp.assert_node_ids("User", &[1]); resp.assert_node_ids("Group", &[100]); resp.assert_node_absent("User", 2); resp.assert_node_absent("Group", 102); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); Loading Loading @@ -742,8 +728,7 @@ async fn neighbors_outgoing_returns_correct_targets(ctx: &TestContext) { resp.assert_referential_integrity(); let neighbor_ids = resp.node_ids("Group"); assert_eq!(neighbor_ids, HashSet::from([100, 102])); resp.assert_node_ids("Group", &[100, 102]); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); resp.assert_edge_exists("User", 1, "Group", 102, "MEMBER_OF"); Loading @@ -767,8 +752,7 @@ async fn neighbors_incoming_returns_correct_sources(ctx: &TestContext) { ) .await; let user_ids = resp.node_ids("User"); assert_eq!(user_ids, HashSet::from([1, 2])); resp.assert_node_ids("User", &[1, 2]); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); resp.assert_edge_exists("User", 2, "Group", 100, "MEMBER_OF"); Loading @@ -787,11 +771,8 @@ async fn neighbors_rel_types_filter_works(ctx: &TestContext) { ) .await; let project_ids = resp.node_ids("Project"); assert_eq!(project_ids, HashSet::from([1000, 1002])); let edges = resp.edges_of_type("CONTAINS"); assert_eq!(edges.len(), 2, "should have exactly 2 CONTAINS edges"); resp.assert_node_ids("Project", &[1000, 1002]); resp.assert_edge_count("CONTAINS", 2); } async fn neighbors_both_direction_returns_all_connected(ctx: &TestContext) { Loading @@ -807,11 +788,8 @@ async fn neighbors_both_direction_returns_all_connected(ctx: &TestContext) { ) .await; let user_ids = resp.node_ids("User"); let project_ids = resp.node_ids("Project"); assert_eq!(user_ids, HashSet::from([1, 2])); assert_eq!(project_ids, HashSet::from([1000, 1002])); resp.assert_node_ids("User", &[1, 2]); resp.assert_node_ids("Project", &[1000, 1002]); resp.assert_referential_integrity(); resp.assert_edge_exists("User", 1, "Group", 100, "MEMBER_OF"); Loading