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

fix(dx): tighten YAML scenario assertions and add path-finding harness features

parent 5f0698eb
Loading
Loading
Loading
Loading
+38 −0
Changes for crates/integration-testkit/src/query_scenario/format.rs: 38 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -78,6 +78,9 @@ pub struct QueryExpect {
    pub compile_only: bool,
    #[serde(default)]
    pub compile_error: Option<CompileErrorExpect>,
    /// Assert error message does NOT contain these substrings.
    #[serde(default)]
    pub compile_error_not_contains: Vec<String>,
    #[serde(default)]
    pub node_count: Option<usize>,
    #[serde(default)]
@@ -107,9 +110,24 @@ pub struct QueryExpect {
    pub sql_contains: Vec<String>,
    #[serde(default)]
    pub sql_not_contains: Vec<String>,
    /// Assert total edge count across all types.
    #[serde(default)]
    pub total_edge_count: Option<usize>,
    /// Assert the number of paths returned by a path_finding query.
    #[serde(default)]
    pub path_count: Option<usize>,
    /// Assert path destinations: `{ Project: [1000, 1004] }`.
    /// Collects the `to_id` of the last edge in each path, grouped by
    /// `to` entity type. Compared as sorted sets.
    #[serde(default)]
    pub path_destinations: BTreeMap<String, Vec<i64>>,
    /// Assert per-path edge structure: each entry is one path's edges
    /// in step order. `{ from: User, from_id: 1, type: MEMBER_OF, to: Group, to_id: 100 }`
    #[serde(default)]
    pub path_edges: Vec<Vec<PathEdgeExpect>>,
    /// Assert entities that must NOT appear as path edge endpoints.
    #[serde(default)]
    pub path_endpoint_absent: Vec<String>,
    #[serde(default)]
    pub referential_integrity: bool,
    #[serde(default)]
@@ -223,7 +241,11 @@ impl QueryExpect {
            || self.empty_aggregation
            || self.row_count.is_some()
            || !self.row_values.is_empty()
            || self.total_edge_count.is_some()
            || self.path_count.is_some()
            || !self.path_destinations.is_empty()
            || !self.path_edges.is_empty()
            || !self.path_endpoint_absent.is_empty()
            || self.referential_integrity
            || self.has_more.is_some();
        assert!(
@@ -256,3 +278,19 @@ pub enum CompileErrorExpect {
    Flag(bool),
    Substring(String),
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathEdgeExpect {
    #[serde(default)]
    pub from: Option<String>,
    #[serde(default)]
    pub from_id: Option<i64>,
    #[serde(rename = "type")]
    #[serde(default)]
    pub edge_type: Option<String>,
    #[serde(default)]
    pub to: Option<String>,
    #[serde(default)]
    pub to_id: Option<i64>,
}
+97 −4
Changes for crates/integration-testkit/src/query_scenario/mod.rs: 97 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -22,7 +22,8 @@ use crate::visitor::{NodeExt, Requirement, ResponseView};
use crate::{SeededColumnResolver, collect_subtest_results, load_ontology};

pub use format::{
    PresetOr, QueryExpect, QueryScenario, RedactionConfig, ScenarioConfig, SecurityOverride,
    PathEdgeExpect, PresetOr, QueryExpect, QueryScenario, RedactionConfig, ScenarioConfig,
    SecurityOverride,
};

use orbit_server::pipeline::HydrationStage;
@@ -192,12 +193,28 @@ async fn run_frontend(
            Arc::new(c)
        }
        Err(e) => match &expect.compile_error {
            Some(format::CompileErrorExpect::Flag(true)) => return,
            Some(format::CompileErrorExpect::Flag(true)) => {
                let msg = e.to_string();
                for banned in &expect.compile_error_not_contains {
                    assert!(
                        !msg.contains(banned.as_str()),
                        "{label}: compile error must not contain '{banned}'\nerror: {msg}"
                    );
                }
                return;
            }
            Some(format::CompileErrorExpect::Substring(sub)) => {
                let msg = e.to_string();
                assert!(
                    e.to_string().contains(sub.as_str()),
                    "{label}: compile error '{e}' does not contain '{sub}'"
                    msg.contains(sub.as_str()),
                    "{label}: compile error '{msg}' does not contain '{sub}'"
                );
                for banned in &expect.compile_error_not_contains {
                    assert!(
                        !msg.contains(banned.as_str()),
                        "{label}: compile error must not contain '{banned}'\nerror: {msg}"
                    );
                }
                return;
            }
            _ => panic!("{label}: unexpected compile error: {e}"),
@@ -556,6 +573,13 @@ fn apply_expect(view: &ResponseView, expect: &QueryExpect, label: &str) {
    for (kind, count) in &expect.edge_count {
        view.assert_edge_count(kind, *count);
    }
    if let Some(n) = expect.total_edge_count {
        assert_eq!(
            view.response.edges.len(),
            n,
            "{label}: total edge count mismatch"
        );
    }
    for (group_key, ge) in &expect.groups {
        if let (Some(entity), Some(order)) = (&ge.entity, &ge.order) {
            view.assert_group_node_order(group_key, entity, order);
@@ -612,6 +636,75 @@ fn apply_expect(view: &ResponseView, expect: &QueryExpect, label: &str) {
        let pids = view.path_ids();
        assert_eq!(pids.len(), n, "{label}: path count mismatch");
    }
    if !expect.path_destinations.is_empty() {
        let pids = view.path_ids();
        let mut actual: std::collections::BTreeMap<String, Vec<i64>> =
            std::collections::BTreeMap::new();
        for &pid in pids.iter() {
            if let Some(last) = view.path(pid).last() {
                actual.entry(last.to.clone()).or_default().push(last.to_id);
            }
        }
        for vals in actual.values_mut() {
            vals.sort();
            vals.dedup();
        }
        for (entity, expected_ids) in &expect.path_destinations {
            let mut expected = expected_ids.clone();
            expected.sort();
            let got = actual.get(entity).cloned().unwrap_or_default();
            assert_eq!(
                got, expected,
                "{label}: path destinations for {entity} mismatch"
            );
        }
    }
    if !expect.path_edges.is_empty() {
        let pids = view.path_ids();
        assert_eq!(
            pids.len(),
            expect.path_edges.len(),
            "{label}: path_edges count ({}) != actual path count ({})",
            expect.path_edges.len(),
            pids.len()
        );
        for (i, (&pid, expected_edges)) in pids.iter().zip(&expect.path_edges).enumerate() {
            let actual = view.path(pid);
            assert_eq!(
                actual.len(),
                expected_edges.len(),
                "{label}: path {i} edge count mismatch"
            );
            for (j, (edge, exp)) in actual.iter().zip(expected_edges).enumerate() {
                if let Some(ref from) = exp.from {
                    assert_eq!(&edge.from, from, "{label}: path {i} edge {j} from entity");
                }
                if let Some(from_id) = exp.from_id {
                    assert_eq!(edge.from_id, from_id, "{label}: path {i} edge {j} from_id");
                }
                if let Some(ref t) = exp.edge_type {
                    assert_eq!(&edge.edge_type, t, "{label}: path {i} edge {j} type");
                }
                if let Some(ref to) = exp.to {
                    assert_eq!(&edge.to, to, "{label}: path {i} edge {j} to entity");
                }
                if let Some(to_id) = exp.to_id {
                    assert_eq!(edge.to_id, to_id, "{label}: path {i} edge {j} to_id");
                }
            }
        }
    }
    for banned in &expect.path_endpoint_absent {
        for edge in &view.response.edges {
            if edge.path_id.is_some() {
                assert_ne!(
                    &edge.to, banned,
                    "{label}: path edge must not target {banned} (got {}->{} via {})",
                    edge.from_id, edge.to_id, edge.edge_type
                );
            }
        }
    }
    if expect.referential_integrity {
        view.assert_referential_integrity();
    }
+1 −1
Changes for crates/integration-tests/tests/server/data_correctness/scenarios/aggregation/empty_security_compile_error.yaml: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -21,4 +21,4 @@ query:
    }

expect:
  compile_error: true
  compile_error: "traversal_path"
+31 −0
Changes for crates/integration-tests/tests/server/data_correctness/scenarios/aggregation/redaction_excludes_unauthorized.yaml: 31 added lines, 0 removed lines.
Original line number Diff line number Diff line
# source: crates/integration-tests/tests/server/data_correctness/aggregation.rs::aggregation_redaction_excludes_unauthorized_from_counts
description: Aggregation counts survive redaction because they are computed in SQL before entity-level row removal

config:
  redaction:
    allow:
      user: [1, 2]
      group: [100, 101, 102, 200, 300]

query:
  json: |
    {
      "query_type": "aggregation",
      "nodes": [
        {"id": "g", "entity": "Group", "id_range": {"start": 1, "end": 10000}, "columns": ["name"]},
        {"id": "u", "entity": "User"}
      ],
      "relationships": [{"type": "MEMBER_OF", "from": "u", "to": "g"}],
      "group_by": ["g"],
      "aggregations": [{"count": "u", "as": "member_count"}],
      "limit": 10
    }

expect:
  groups:
    g:
      rows:
        - entity: Group
          id: 100
          values:
            member_count: 3
+1 −0
Changes for crates/integration-tests/tests/server/data_correctness/scenarios/edge_cases/empty_result_schema.yaml: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -12,4 +12,5 @@ query:
expect:
  skip_requirements: [node_ids]
  node_count: 0
  total_edge_count: 0
Loading