1use graphql_parser::query::{
8 Definition, Document, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
9};
10
11pub const DEFAULT_MAX_ALIASES: usize = 30;
16
17pub const MAX_VARIABLES_COUNT: usize = 1_000;
23
24#[derive(Debug, Clone)]
26pub struct ComplexityConfig {
27 pub max_depth: usize,
29 pub max_complexity: usize,
31 pub max_aliases: usize,
33}
34
35impl Default for ComplexityConfig {
36 fn default() -> Self {
37 Self {
38 max_depth: 10,
39 max_complexity: 100,
40 max_aliases: DEFAULT_MAX_ALIASES,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct QueryMetrics {
48 pub depth: usize,
50 pub complexity: usize,
52 pub alias_count: usize,
54}
55
56#[derive(Debug, thiserror::Error, Clone)]
58#[non_exhaustive]
59pub enum ComplexityValidationError {
60 #[error("Query exceeds maximum depth of {max_depth}: depth = {actual_depth}")]
62 QueryTooDeep {
63 max_depth: usize,
65 actual_depth: usize,
67 },
68
69 #[error("Query exceeds maximum complexity of {max_complexity}: score = {actual_complexity}")]
71 QueryTooComplex {
72 max_complexity: usize,
74 actual_complexity: usize,
76 },
77
78 #[error("Query exceeds maximum alias count of {max_aliases}: count = {actual_aliases}")]
80 TooManyAliases {
81 max_aliases: usize,
83 actual_aliases: usize,
85 },
86
87 #[error("Invalid variables: {0}")]
89 InvalidVariables(String),
90
91 #[error("Malformed GraphQL query: {0}")]
93 MalformedQuery(String),
94}
95
96#[derive(Debug, Clone)]
102pub struct RequestValidator {
103 max_depth: usize,
105 max_complexity: usize,
107 max_aliases_per_query: usize,
109 validate_depth: bool,
111 validate_complexity: bool,
113}
114
115impl RequestValidator {
116 #[must_use]
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 #[must_use]
124 pub const fn from_config(config: &ComplexityConfig) -> Self {
125 Self {
126 max_depth: config.max_depth,
127 max_complexity: config.max_complexity,
128 max_aliases_per_query: config.max_aliases,
129 validate_depth: true,
130 validate_complexity: true,
131 }
132 }
133
134 #[must_use]
136 pub const fn with_max_depth(mut self, max_depth: usize) -> Self {
137 self.max_depth = max_depth;
138 self
139 }
140
141 #[must_use]
143 pub const fn with_max_complexity(mut self, max_complexity: usize) -> Self {
144 self.max_complexity = max_complexity;
145 self
146 }
147
148 #[must_use]
150 pub const fn with_depth_validation(mut self, enabled: bool) -> Self {
151 self.validate_depth = enabled;
152 self
153 }
154
155 #[must_use]
157 pub const fn with_complexity_validation(mut self, enabled: bool) -> Self {
158 self.validate_complexity = enabled;
159 self
160 }
161
162 #[must_use]
164 pub const fn with_max_aliases(mut self, max_aliases: usize) -> Self {
165 self.max_aliases_per_query = max_aliases;
166 self
167 }
168
169 pub fn analyze(&self, query: &str) -> Result<QueryMetrics, ComplexityValidationError> {
179 if query.trim().is_empty() {
180 return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
181 }
182 let document = graphql_parser::parse_query::<String>(query)
183 .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
184 let fragments = collect_fragments(&document);
185 Ok(QueryMetrics {
186 depth: self.calculate_depth_ast(&document, &fragments),
187 complexity: self.calculate_complexity_ast(&document, &fragments),
188 alias_count: self.count_aliases_ast(&document),
189 })
190 }
191
192 pub fn validate_query(&self, query: &str) -> Result<(), ComplexityValidationError> {
198 if query.trim().is_empty() {
199 return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
200 }
201
202 if !self.validate_depth && !self.validate_complexity && self.max_aliases_per_query == 0 {
206 return Ok(());
207 }
208
209 let document = graphql_parser::parse_query::<String>(query)
210 .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
211 let fragments = collect_fragments(&document);
212
213 if self.validate_depth {
214 let depth = self.calculate_depth_ast(&document, &fragments);
215 if depth > self.max_depth {
216 return Err(ComplexityValidationError::QueryTooDeep {
217 max_depth: self.max_depth,
218 actual_depth: depth,
219 });
220 }
221 }
222
223 if self.validate_complexity {
224 let complexity = self.calculate_complexity_ast(&document, &fragments);
225 if complexity > self.max_complexity {
226 return Err(ComplexityValidationError::QueryTooComplex {
227 max_complexity: self.max_complexity,
228 actual_complexity: complexity,
229 });
230 }
231 }
232
233 let alias_count = self.count_aliases_ast(&document);
234 if alias_count > self.max_aliases_per_query {
235 return Err(ComplexityValidationError::TooManyAliases {
236 max_aliases: self.max_aliases_per_query,
237 actual_aliases: alias_count,
238 });
239 }
240
241 Ok(())
242 }
243
244 pub fn validate_variables(
256 &self,
257 variables: Option<&serde_json::Value>,
258 ) -> Result<(), ComplexityValidationError> {
259 if let Some(vars) = variables {
260 if !vars.is_object() {
261 return Err(ComplexityValidationError::InvalidVariables(
262 "Variables must be an object".to_string(),
263 ));
264 }
265 let obj = vars.as_object().expect("invariant: vars.is_object() checked above");
268 if obj.len() > MAX_VARIABLES_COUNT {
269 return Err(ComplexityValidationError::InvalidVariables(format!(
270 "Too many variables: {} exceeds maximum of {}",
271 obj.len(),
272 MAX_VARIABLES_COUNT
273 )));
274 }
275 }
276 Ok(())
277 }
278
279 fn calculate_depth_ast(
280 &self,
281 document: &Document<String>,
282 fragments: &[&FragmentDefinition<String>],
283 ) -> usize {
284 document
285 .definitions
286 .iter()
287 .map(|def| match def {
288 Definition::Operation(op) => match op {
289 OperationDefinition::Query(q) => {
290 self.selection_set_depth(&q.selection_set, fragments, 0)
291 },
292 OperationDefinition::Mutation(m) => {
293 self.selection_set_depth(&m.selection_set, fragments, 0)
294 },
295 OperationDefinition::Subscription(s) => {
296 self.selection_set_depth(&s.selection_set, fragments, 0)
297 },
298 OperationDefinition::SelectionSet(ss) => {
299 self.selection_set_depth(ss, fragments, 0)
300 },
301 },
302 Definition::Fragment(f) => self.selection_set_depth(&f.selection_set, fragments, 0),
303 })
304 .max()
305 .unwrap_or(0)
306 }
307
308 fn selection_set_depth(
309 &self,
310 selection_set: &SelectionSet<String>,
311 fragments: &[&FragmentDefinition<String>],
312 recursion_depth: usize,
313 ) -> usize {
314 if recursion_depth > 32 {
315 return self.max_depth + 1;
316 }
317 if selection_set.items.is_empty() {
318 return 0;
319 }
320 let max_child = selection_set
321 .items
322 .iter()
323 .map(|sel| match sel {
324 Selection::Field(field) => {
325 if field.selection_set.items.is_empty() {
326 0
327 } else {
328 self.selection_set_depth(&field.selection_set, fragments, recursion_depth)
329 }
330 },
331 Selection::InlineFragment(inline) => {
332 self.selection_set_depth(&inline.selection_set, fragments, recursion_depth)
333 },
334 Selection::FragmentSpread(spread) => {
335 if let Some(frag) = fragments.iter().find(|f| f.name == spread.fragment_name) {
336 self.selection_set_depth(
337 &frag.selection_set,
338 fragments,
339 recursion_depth + 1,
340 )
341 } else {
342 self.max_depth
343 }
344 },
345 })
346 .max()
347 .unwrap_or(0);
348 1 + max_child
349 }
350
351 fn calculate_complexity_ast(
352 &self,
353 document: &Document<String>,
354 fragments: &[&FragmentDefinition<String>],
355 ) -> usize {
356 document
357 .definitions
358 .iter()
359 .map(|def| match def {
360 Definition::Operation(op) => match op {
361 OperationDefinition::Query(q) => {
362 self.selection_set_complexity(&q.selection_set, fragments, 0)
363 },
364 OperationDefinition::Mutation(m) => {
365 self.selection_set_complexity(&m.selection_set, fragments, 0)
366 },
367 OperationDefinition::Subscription(s) => {
368 self.selection_set_complexity(&s.selection_set, fragments, 0)
369 },
370 OperationDefinition::SelectionSet(ss) => {
371 self.selection_set_complexity(ss, fragments, 0)
372 },
373 },
374 Definition::Fragment(_) => 0,
375 })
376 .sum()
377 }
378
379 fn selection_set_complexity(
380 &self,
381 selection_set: &SelectionSet<String>,
382 fragments: &[&FragmentDefinition<String>],
383 recursion_depth: usize,
384 ) -> usize {
385 if recursion_depth > 32 {
386 return self.max_complexity + 1;
387 }
388 selection_set
389 .items
390 .iter()
391 .map(|sel| match sel {
392 Selection::Field(field) => {
393 let multiplier = extract_limit_multiplier(&field.arguments);
394 if field.selection_set.items.is_empty() {
395 1
396 } else {
397 let nested = self.selection_set_complexity(
398 &field.selection_set,
399 fragments,
400 recursion_depth,
401 );
402 1 + nested * multiplier
403 }
404 },
405 Selection::InlineFragment(inline) => {
406 self.selection_set_complexity(&inline.selection_set, fragments, recursion_depth)
407 },
408 Selection::FragmentSpread(spread) => {
409 if let Some(frag) = fragments.iter().find(|f| f.name == spread.fragment_name) {
410 self.selection_set_complexity(
411 &frag.selection_set,
412 fragments,
413 recursion_depth + 1,
414 )
415 } else {
416 10
417 }
418 },
419 })
420 .sum()
421 }
422
423 fn count_aliases_ast(&self, document: &Document<String>) -> usize {
424 document
425 .definitions
426 .iter()
427 .map(|def| match def {
428 Definition::Operation(op) => {
429 let ss = match op {
430 OperationDefinition::Query(q) => &q.selection_set,
431 OperationDefinition::Mutation(m) => &m.selection_set,
432 OperationDefinition::Subscription(s) => &s.selection_set,
433 OperationDefinition::SelectionSet(ss) => ss,
434 };
435 count_aliases_in_selection_set(ss)
436 },
437 Definition::Fragment(f) => count_aliases_in_selection_set(&f.selection_set),
438 })
439 .sum()
440 }
441}
442
443impl Default for RequestValidator {
444 fn default() -> Self {
445 Self {
446 max_depth: 10,
447 max_complexity: 100,
448 max_aliases_per_query: DEFAULT_MAX_ALIASES,
449 validate_depth: true,
450 validate_complexity: true,
451 }
452 }
453}
454
455fn collect_fragments<'a>(
457 document: &'a Document<'a, String>,
458) -> Vec<&'a FragmentDefinition<'a, String>> {
459 document
460 .definitions
461 .iter()
462 .filter_map(|def| {
463 if let Definition::Fragment(f) = def {
464 Some(f)
465 } else {
466 None
467 }
468 })
469 .collect()
470}
471
472fn extract_limit_multiplier(arguments: &[(String, graphql_parser::query::Value<String>)]) -> usize {
474 for (name, value) in arguments {
475 if matches!(name.as_str(), "first" | "limit" | "take" | "last") {
476 if let graphql_parser::query::Value::Int(n) = value {
477 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
478 let limit = n.as_i64().unwrap_or(10) as usize;
481 return limit.clamp(1, 100);
482 }
483 }
484 }
485 1
486}
487
488fn count_aliases_in_selection_set(selection_set: &SelectionSet<String>) -> usize {
490 selection_set
491 .items
492 .iter()
493 .map(|sel| match sel {
494 Selection::Field(field) => {
495 let self_alias = usize::from(field.alias.is_some());
496 self_alias + count_aliases_in_selection_set(&field.selection_set)
497 },
498 Selection::InlineFragment(inline) => {
499 count_aliases_in_selection_set(&inline.selection_set)
500 },
501 Selection::FragmentSpread(_) => 0,
502 })
503 .sum()
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
513 fn test_operation_name_not_counted_as_field() {
514 let validator = RequestValidator::default();
515 let metrics = validator
516 .analyze("query getUserPosts { users { id name } }")
517 .expect("valid query");
518 assert!(
521 metrics.complexity <= 10,
522 "operation name must not inflate complexity; got {metrics:?}"
523 );
524 }
525
526 #[test]
527 fn test_arguments_not_counted_as_fields() {
528 let validator = RequestValidator::default();
529 let metrics = validator
530 .analyze("{ users(limit: 10, offset: 0) { id } }")
531 .expect("valid query");
532 assert!(
534 metrics.complexity < 50,
535 "arguments must not be counted as fields; got {metrics:?}"
536 );
537 }
538
539 #[test]
542 fn test_simple_query_depth() {
543 let validator = RequestValidator::default();
544 let metrics = validator.analyze("{ users { id name } }").expect("valid");
545 assert_eq!(metrics.depth, 2);
546 }
547
548 #[test]
549 fn test_deeply_nested_query_depth() {
550 let validator = RequestValidator::default();
551 let query = "{ a { b { c { d { e { f { g { h } } } } } } } }";
552 let metrics = validator.analyze(query).expect("valid");
553 assert!(metrics.depth >= 8, "expected depth ≥ 8, got {}", metrics.depth);
554 }
555
556 #[test]
557 fn test_depth_validation_pass() {
558 let validator = RequestValidator::default().with_max_depth(5);
559 validator
560 .validate_query("{ user { id } }")
561 .unwrap_or_else(|e| panic!("expected Ok: {e}"));
562 }
563
564 #[test]
565 fn test_depth_validation_fail() {
566 let validator = RequestValidator::default().with_max_depth(3);
567 let deep = "{ user { profile { settings { theme } } } }";
568 let result = validator.validate_query(deep);
569 assert!(
570 matches!(result, Err(ComplexityValidationError::QueryTooDeep { .. })),
571 "expected QueryTooDeep, got: {result:?}"
572 );
573 }
574
575 #[test]
578 fn test_fragment_depth_bypass_blocked() {
579 let validator = RequestValidator::new().with_max_depth(3);
580 let query = "
581 fragment Deep on User { a { b { c { d { e } } } } }
582 query { ...Deep }
583 ";
584 assert!(
585 validator.validate_query(query).is_err(),
586 "fragment depth bypass must be blocked"
587 );
588 }
589
590 #[test]
591 fn test_shallow_fragment_allowed() {
592 let validator = RequestValidator::new().with_max_depth(5);
593 let query = "
594 fragment UserFields on User { id name email }
595 query { user { ...UserFields } }
596 ";
597 validator.validate_query(query).unwrap_or_else(|e| panic!("expected Ok: {e}"));
598 }
599
600 #[test]
603 fn test_complexity_validation_pass() {
604 let validator = RequestValidator::default().with_max_complexity(20);
605 validator
606 .validate_query("query { user { id name email } }")
607 .unwrap_or_else(|e| panic!("expected Ok: {e}"));
608 }
609
610 #[test]
611 fn test_pagination_limit_multiplier() {
612 let validator = RequestValidator::new().with_max_complexity(50);
613 let query = "query { users(first: 100) { id name } }";
614 assert!(
615 validator.validate_query(query).is_err(),
616 "high pagination limits must increase complexity"
617 );
618 }
619
620 #[test]
621 fn test_nested_list_multiplier() {
622 let validator = RequestValidator::new().with_max_complexity(50);
623 let query = "query { users(first: 10) { friends(first: 10) { id } } }";
624 assert!(
625 validator.validate_query(query).is_err(),
626 "nested list multipliers must compound"
627 );
628 }
629
630 #[test]
633 fn test_alias_count_within_limit() {
634 let validator = RequestValidator::new().with_max_aliases(5);
635 let query = "query { a: user { id } b: user { id } c: user { id } }";
636 validator.validate_query(query).unwrap_or_else(|e| panic!("expected Ok: {e}"));
637 }
638
639 #[test]
640 fn test_alias_count_exceeds_limit() {
641 let validator = RequestValidator::new().with_max_aliases(2);
642 let query = "query { a: user { id } b: user { id } c: user { id } }";
643 assert!(
644 matches!(
645 validator.validate_query(query),
646 Err(ComplexityValidationError::TooManyAliases {
647 actual_aliases: 3,
648 ..
649 })
650 ),
651 "should report alias count"
652 );
653 }
654
655 #[test]
656 fn test_default_alias_limit_is_30() {
657 let validator = RequestValidator::new();
658 let fields_30: String = (0..30).fold(String::new(), |mut s, i| {
659 use std::fmt::Write;
660 let _ = write!(s, "f{i}: user {{ id }} ");
661 s
662 });
663 validator
664 .validate_query(&format!("query {{ {fields_30} }}"))
665 .unwrap_or_else(|e| panic!("expected Ok for 30 aliases: {e}"));
666
667 let fields_31: String = (0..31).fold(String::new(), |mut s, i| {
668 use std::fmt::Write;
669 let _ = write!(s, "f{i}: user {{ id }} ");
670 s
671 });
672 let result_31 = validator.validate_query(&format!("query {{ {fields_31} }}"));
673 assert!(
674 matches!(result_31, Err(ComplexityValidationError::TooManyAliases { .. })),
675 "expected TooManyAliases for 31 aliases, got: {result_31:?}"
676 );
677 }
678
679 #[test]
682 fn test_empty_query_rejected() {
683 let validator = RequestValidator::new();
684 let r1 = validator.validate_query("");
685 assert!(
686 matches!(r1, Err(ComplexityValidationError::MalformedQuery(_))),
687 "expected MalformedQuery for empty string, got: {r1:?}"
688 );
689 let r2 = validator.validate_query(" ");
690 assert!(
691 matches!(r2, Err(ComplexityValidationError::MalformedQuery(_))),
692 "expected MalformedQuery for whitespace, got: {r2:?}"
693 );
694 }
695
696 #[test]
697 fn test_malformed_query_rejected() {
698 let validator = RequestValidator::new();
699 let result = validator.validate_query("{ invalid query {{}}");
700 assert!(
701 matches!(result, Err(ComplexityValidationError::MalformedQuery(_))),
702 "expected MalformedQuery, got: {result:?}"
703 );
704 }
705
706 #[test]
709 fn test_valid_variables() {
710 let validator = RequestValidator::new();
711 let vars = serde_json::json!({"id": "123"});
712 validator
713 .validate_variables(Some(&vars))
714 .unwrap_or_else(|e| panic!("expected Ok: {e}"));
715 }
716
717 #[test]
718 fn test_invalid_variables_not_object() {
719 let validator = RequestValidator::new();
720 let vars = serde_json::json!([1, 2, 3]);
721 let result = validator.validate_variables(Some(&vars));
722 assert!(
723 matches!(result, Err(ComplexityValidationError::InvalidVariables(_))),
724 "expected InvalidVariables, got: {result:?}"
725 );
726 }
727
728 #[test]
729 fn test_validate_variables_too_many() {
730 let validator = RequestValidator::new();
731 let vars: serde_json::Value = serde_json::Value::Object(
733 (0..=MAX_VARIABLES_COUNT)
734 .map(|i| (format!("v{i}"), serde_json::Value::Null))
735 .collect(),
736 );
737 let result = validator.validate_variables(Some(&vars));
738 assert!(
739 matches!(result, Err(ComplexityValidationError::InvalidVariables(_))),
740 "expected InvalidVariables for too-many-variables, got: {result:?}"
741 );
742 }
743
744 #[test]
745 fn test_validate_variables_at_limit_is_ok() {
746 let validator = RequestValidator::new();
747 let vars: serde_json::Value = serde_json::Value::Object(
749 (0..MAX_VARIABLES_COUNT)
750 .map(|i| (format!("v{i}"), serde_json::Value::Null))
751 .collect(),
752 );
753 validator
754 .validate_variables(Some(&vars))
755 .unwrap_or_else(|e| panic!("expected Ok at limit, got: {e}"));
756 }
757
758 #[test]
761 fn test_disable_depth_and_complexity_validation() {
762 let validator = RequestValidator::new()
763 .with_depth_validation(false)
764 .with_complexity_validation(false)
765 .with_max_depth(1)
766 .with_max_complexity(1);
767 let deep = "{ a { b { c { d { e { f } } } } } }";
768 validator
769 .validate_query(deep)
770 .unwrap_or_else(|e| panic!("expected Ok when depth/complexity disabled: {e}"));
771 }
772
773 #[test]
780 fn test_complexity_at_limit_is_allowed() {
781 let validator = RequestValidator::new().with_max_complexity(3);
783 validator
784 .validate_query("query { a b c }")
785 .unwrap_or_else(|e| panic!("complexity == max must be allowed: {e}"));
786 }
787
788 #[test]
789 fn test_complexity_just_over_limit_is_rejected() {
790 let validator = RequestValidator::new().with_max_complexity(3);
792 assert!(
793 matches!(
794 validator.validate_query("query { a b c d }"),
795 Err(ComplexityValidationError::QueryTooComplex { .. })
796 ),
797 "complexity > max must be rejected"
798 );
799 }
800
801 #[test]
803 fn test_depth_at_limit_is_allowed() {
804 let validator = RequestValidator::default().with_max_depth(3);
806 validator
807 .validate_query("{ a { b { c } } }")
808 .unwrap_or_else(|e| panic!("depth == max must be allowed: {e}"));
809 }
810
811 #[test]
812 fn test_depth_just_over_limit_is_rejected() {
813 let validator = RequestValidator::default().with_max_depth(3);
815 assert!(
816 matches!(
817 validator.validate_query("{ a { b { c { d } } } }"),
818 Err(ComplexityValidationError::QueryTooDeep { .. })
819 ),
820 "depth > max must be rejected"
821 );
822 }
823
824 #[test]
826 fn test_skip_validation_requires_aliases_also_zero() {
827 let validator = RequestValidator::new()
830 .with_depth_validation(false)
831 .with_complexity_validation(false)
832 .with_max_aliases(2);
833 let query = "query { a: user { id } b: user { id } c: user { id } }";
834 assert!(
835 validator.validate_query(query).is_err(),
836 "alias check must run even when depth/complexity validation is disabled"
837 );
838 }
839
840 #[test]
841 fn test_early_return_requires_depth_disabled() {
842 let validator = RequestValidator::new()
845 .with_depth_validation(true)
846 .with_complexity_validation(false)
847 .with_max_aliases(0)
848 .with_max_depth(2);
849 assert!(
850 matches!(
851 validator.validate_query("{ a { b { c } } }"),
852 Err(ComplexityValidationError::QueryTooDeep { .. })
853 ),
854 "depth validation must still run when only complexity is disabled"
855 );
856 }
857
858 #[test]
859 fn test_early_return_requires_complexity_disabled() {
860 let validator = RequestValidator::new()
863 .with_depth_validation(false)
864 .with_complexity_validation(true)
865 .with_max_aliases(0)
866 .with_max_complexity(2);
867 assert!(
868 matches!(
869 validator.validate_query("query { users(first: 100) { id name } }"),
870 Err(ComplexityValidationError::QueryTooComplex { .. })
871 ),
872 "complexity validation must still run when only depth is disabled"
873 );
874 }
875
876 #[test]
878 fn test_deep_fragment_recursion_guard() {
879 let validator = RequestValidator::new().with_max_depth(5);
883 let mut query = String::from("query { ...F0 }\n");
884 for i in 0..34_usize {
885 use std::fmt::Write;
886 let _ = writeln!(query, "fragment F{i} on T {{ ...F{} }}", i + 1);
887 }
888 query.push_str("fragment F34 on T { id }\n");
889 assert!(
890 validator.validate_query(&query).is_err(),
891 "deeply nested fragment chain must be rejected by recursion guard"
892 );
893 }
894
895 #[test]
897 fn test_nested_aliases_counted_correctly() {
898 let validator = RequestValidator::new().with_max_aliases(3);
901 assert!(
902 matches!(
903 validator.validate_query("query { a: user { id } b: user { c: name d: email } }"),
904 Err(ComplexityValidationError::TooManyAliases {
905 actual_aliases: 4,
906 ..
907 })
908 ),
909 "nested aliases must be summed, not subtracted"
910 );
911 }
912
913 #[test]
916 fn test_from_config() {
917 let config = ComplexityConfig {
918 max_depth: 5,
919 max_complexity: 20,
920 max_aliases: 3,
921 };
922 let validator = RequestValidator::from_config(&config);
923 let result = validator.validate_query("{ a { b { c { d { e { f } } } } } }");
925 assert!(
926 matches!(result, Err(ComplexityValidationError::QueryTooDeep { .. })),
927 "expected QueryTooDeep for depth-6 query with max 5, got: {result:?}"
928 );
929 }
930}