Verified Commit fedad0b7 authored by Michael Usachenko's avatar Michael Usachenko Committed by GitLab
Browse files

chore(cleanup): continue hardening data correctness harness + remove cruft

parent 5658ea2b
Loading
Loading
Loading
Loading
+122 −2
Original line number Diff line number Diff line
@@ -254,6 +254,7 @@ mod tests {
        );
        let reqs = input.requirements();
        assert!(reqs.contains(&Requirement::NodeIds));
        assert_eq!(reqs.len(), 1);
    }

    #[test]
@@ -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]
@@ -284,6 +286,7 @@ mod tests {
        );
        let reqs = input.requirements();
        assert!(reqs.contains(&Requirement::Aggregation));
        assert_eq!(reqs.len(), 1);
    }

    #[test]
@@ -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]
@@ -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]
@@ -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 ────────────────────────────────────────
@@ -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]
@@ -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]
@@ -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]
+52 −104
Original line number Diff line number Diff line
@@ -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) {
@@ -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`].
@@ -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::*;
@@ -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]
@@ -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]
+21 −43
Original line number Diff line number Diff line
@@ -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("");
@@ -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");
@@ -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),
@@ -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();
@@ -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) {
@@ -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");
@@ -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");
@@ -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");
@@ -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) {
@@ -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");