ecat_graphql/
validation.rs1use crate::parser::{ParsedField, SelectionSet};
6
7pub const DEFAULT_MAX_COMPLEXITY: usize = 10_000;
8
9#[derive(Debug, Clone, Copy)]
10pub struct QueryLimits {
11 pub max_complexity: usize,
13}
14
15impl Default for QueryLimits {
16 fn default() -> Self {
17 Self {
18 max_complexity: DEFAULT_MAX_COMPLEXITY,
19 }
20 }
21}
22
23pub fn validate(field: &ParsedField, limits: &QueryLimits) -> Result<(), String> {
26 let n = complexity(&field.selection) + 1;
27 if n > limits.max_complexity {
28 Err(format!(
29 "query complexity {n} exceeds limit {}",
30 limits.max_complexity
31 ))
32 } else {
33 Ok(())
34 }
35}
36
37fn complexity(sel: &Option<SelectionSet>) -> usize {
38 match sel {
39 None => 0,
40 Some(set) => set.values().map(|n| 1 + complexity(&n.selection)).sum(),
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47 use crate::parser::parse_query;
48 use crate::{GraphQLSchema, execute};
49 use serde_json::Value;
50
51 fn parse(q: &str) -> ParsedField {
52 parse_query(q, &Value::Null).unwrap()
53 }
54
55 fn limits(max: usize) -> QueryLimits {
56 QueryLimits {
57 max_complexity: max,
58 }
59 }
60
61 #[test]
62 fn over_budget_query_is_rejected() {
63 let f = parse("{ a { b { c { x } } d { e } } }");
65 let err = validate(&f, &limits(5)).unwrap_err();
66 assert!(err.contains("complexity 6 exceeds limit 5"), "got: {err}");
67 }
68
69 #[test]
70 fn under_budget_query_passes() {
71 let f = parse("{ a { b { c } d { e } } }");
72 assert!(validate(&f, &limits(5)).is_ok());
73 assert!(validate(&f, &QueryLimits::default()).is_ok());
74 }
75
76 #[test]
77 fn bare_field_costs_one() {
78 let f = parse("{ ping }");
79 assert!(validate(&f, &limits(1)).is_ok());
80 assert!(validate(&f, &limits(0)).is_err());
81 }
82
83 #[test]
84 fn execute_rejects_over_budget_before_resolving() {
85 let schema = GraphQLSchema::new()
86 .with_limits(limits(1))
87 .query_fn("boom", |_v| {
88 Box::pin(async { Ok(serde_json::json!("never")) })
89 });
90 let rt = tokio::runtime::Runtime::new().unwrap();
91 let err = rt
92 .block_on(execute(&schema, "{ boom { x } }", &Value::Null))
93 .unwrap_err();
94 assert!(
95 err.iter().any(|e| e.contains("exceeds limit")),
96 "got: {err:?}"
97 );
98 }
99
100 #[test]
101 fn deeply_nested_query_is_rejected_at_parse() {
102 let deep = "{ a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } }";
103 let err = parse_query(deep, &Value::Null).unwrap_err();
104 assert!(err.contains("selection too deep"), "got: {err}");
105 }
106
107 #[test]
108 fn alias_flood_is_rejected_at_parse() {
109 let err = parse_query("{ a: b c: d e: f g: h i: j k: l m: n }", &Value::Null).unwrap_err();
111 assert!(err.contains("aliases not supported"), "got: {err}");
112 }
113}