Loading crates/integration-testkit/src/query_scenario/format.rs +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 Loading @@ -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)] Loading Loading @@ -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)] Loading Loading @@ -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!( Loading Loading @@ -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>, } crates/integration-testkit/src/query_scenario/mod.rs +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 Loading @@ -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; Loading Loading @@ -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}"), Loading Loading @@ -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); Loading Loading @@ -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(); } Loading crates/integration-tests/tests/server/data_correctness/scenarios/aggregation/empty_security_compile_error.yaml +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 Loading @@ -21,4 +21,4 @@ query: } expect: compile_error: true compile_error: "traversal_path" crates/integration-tests/tests/server/data_correctness/scenarios/aggregation/redaction_excludes_unauthorized.yaml 0 → 100644 +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 crates/integration-tests/tests/server/data_correctness/scenarios/edge_cases/empty_result_schema.yaml +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 Loading @@ -12,4 +12,5 @@ query: expect: skip_requirements: [node_ids] node_count: 0 total_edge_count: 0 Loading
crates/integration-testkit/src/query_scenario/format.rs +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 Loading @@ -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)] Loading Loading @@ -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)] Loading Loading @@ -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!( Loading Loading @@ -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>, }
crates/integration-testkit/src/query_scenario/mod.rs +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 Loading @@ -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; Loading Loading @@ -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}"), Loading Loading @@ -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); Loading Loading @@ -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(); } Loading
crates/integration-tests/tests/server/data_correctness/scenarios/aggregation/empty_security_compile_error.yaml +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 Loading @@ -21,4 +21,4 @@ query: } expect: compile_error: true compile_error: "traversal_path"
crates/integration-tests/tests/server/data_correctness/scenarios/aggregation/redaction_excludes_unauthorized.yaml 0 → 100644 +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
crates/integration-tests/tests/server/data_correctness/scenarios/edge_cases/empty_result_schema.yaml +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 Loading @@ -12,4 +12,5 @@ query: expect: skip_requirements: [node_ids] node_count: 0 total_edge_count: 0