use rstest::rstest;
use super::{parse, parse_err};
use crate::gql::{GqlParserSettings, parse_with_settings};
#[rstest]
#[case::parens_within("(", "1", ")", 99, true)]
#[case::parens_beyond("(", "1", ")", 100, false)]
#[case::lists_within("[", "1", "]", 99, true)]
#[case::lists_beyond("[", "1", "]", 100, false)]
#[case::maps_within("{a: ", "1", "}", 99, true)]
#[case::maps_beyond("{a: ", "1", "}", 100, false)]
#[case::calls_within("f(", "1", ")", 99, true)]
#[case::calls_beyond("f(", "1", ")", 100, false)]
fn nesting_depth_limit(
#[case] open: &str,
#[case] inner: &str,
#[case] close: &str,
#[case] depth: usize,
#[case] ok: bool,
) {
let source = format!("RETURN {}{}{}", open.repeat(depth), inner, close.repeat(depth));
if ok {
parse(&source);
} else {
let error = parse_err(&source);
assert!(error.contains("Exceeded query expression nesting depth limit"), "{error}");
}
}
#[test]
fn label_expression_parens_count_against_the_limit() {
let source = format!("MATCH (a:{}L{}) RETURN 1", "(".repeat(150), ")".repeat(150));
let error = parse_err(&source);
assert!(error.contains("Exceeded query expression nesting depth limit"), "{error}");
}
#[rstest]
#[case::at_limit("RETURN ((((1))))", true)]
#[case::beyond_limit("RETURN (((((1)))))", false)]
fn custom_recursion_limit(#[case] source: &str, #[case] ok: bool) {
let settings = GqlParserSettings {
object_recursion_limit: 5,
..Default::default()
};
let result = parse_with_settings(source, settings);
if ok {
result.expect("should parse within the limit");
} else {
let error = result.expect_err("should exceed the limit");
let rendered = format!("{:?}", error.render_on(source));
assert!(rendered.contains("Exceeded query expression nesting depth limit"), "{rendered}");
}
}
#[test]
fn pathological_open_parens_fail_fast() {
let source = format!("RETURN {}", "(".repeat(100_000));
let error = parse_err(&source);
assert!(error.contains("Exceeded query expression nesting depth limit"), "{error}");
}
#[test]
fn pathological_open_brackets_fail_fast() {
let source = format!("RETURN {}", "[".repeat(100_000));
let error = parse_err(&source);
assert!(error.contains("Exceeded query expression nesting depth limit"), "{error}");
}
#[rstest]
#[case::or_spine(format!("RETURN a{}", " OR a".repeat(200_000)))]
#[case::and_spine(format!("RETURN a{}", " AND a".repeat(200_000)))]
#[case::add_spine(format!("RETURN 1{}", " + 1".repeat(200_000)))]
#[case::mul_spine(format!("RETURN 1{}", " * 1".repeat(200_000)))]
#[case::concat_spine(format!("RETURN a{}", " || a".repeat(200_000)))]
#[case::is_chain(format!("RETURN a{}", " IS NOT FALSE".repeat(200_000)))]
#[case::not_chain(format!("RETURN {}1", "NOT ".repeat(200_000)))]
#[case::unary_minus_chain(format!("RETURN {}1", "- ".repeat(200_000)))]
fn deep_operator_chains_hit_the_expression_depth_limit(#[case] source: String) {
let error = parse_err(&source);
assert!(error.contains("Exceeded expression recursion depth limit"), "{error}");
}
#[test]
fn operator_spines_within_the_default_budget_parse() {
let source = format!("RETURN 1{}", " + 1".repeat(126));
parse(&source);
}
#[test]
fn sibling_expressions_do_not_accumulate_depth() {
let element = format!("1{}", " + 1".repeat(120));
let source = format!("RETURN [{}]", vec![element; 16].join(", "));
parse(&source);
}
#[rstest]
#[case::add_at_limit("RETURN 1 + 1 + 1 + 1 + 1", true)]
#[case::add_beyond_limit("RETURN 1 + 1 + 1 + 1 + 1 + 1", false)]
#[case::or_beyond_limit("RETURN a OR a OR a OR a OR a OR a", false)]
#[case::not_beyond_limit("RETURN NOT NOT NOT NOT NOT 1", false)]
#[case::is_beyond_limit("RETURN a IS TRUE IS TRUE IS TRUE IS TRUE IS TRUE", false)]
fn custom_expression_depth_limit(#[case] source: &str, #[case] ok: bool) {
let settings = GqlParserSettings {
expr_recursion_limit: 5,
..Default::default()
};
let result = parse_with_settings(source, settings);
if ok {
result.expect("should parse within the limit");
} else {
let error = result.expect_err("should exceed the limit");
let rendered = format!("{:?}", error.render_on(source));
assert!(rendered.contains("Exceeded expression recursion depth limit"), "{rendered}");
}
}
#[test]
fn long_property_chain_is_parse_and_drop_safe() {
let source = format!("RETURN a{}", ".b".repeat(200_000));
parse(&source);
}
#[test]
fn deep_is_null_operand_chain_is_drop_safe() {
let source = format!("RETURN a{} IS NOT NULL", ".b".repeat(200_000));
parse(&source);
}
#[test]
fn deep_label_negation_chain_is_parse_and_drop_safe() {
let source = format!("MATCH (a:{}L) RETURN 1", "!".repeat(200_000));
parse(&source);
}
#[test]
fn long_label_operator_chain_is_parse_and_drop_safe() {
let source = format!("MATCH (a:L{}) RETURN 1", "&L|L".repeat(100_000));
parse(&source);
}
#[test]
fn long_function_argument_and_list_trees_are_drop_safe() {
let element = format!("a{}", ".b".repeat(10_000));
let source = format!("RETURN [{}]", vec![element; 20].join(", "));
parse(&source);
let source = format!("RETURN f({}1)", "NOT ".repeat(200_000));
let error = parse_err(&source);
assert!(error.contains("Exceeded expression recursion depth limit"), "{error}");
}
#[test]
fn long_comment_runs_are_iterative() {
let source = format!("{}RETURN 1", "//x\n".repeat(100_000));
parse(&source);
let source = format!("{}RETURN 1", "--x\n".repeat(100_000));
parse(&source);
}