apollo-compiler 1.0.0-beta.15

A compiler for the GraphQL query language.
Documentation
use expect_test::expect;

fn build_fragment_chain(size: usize) -> String {
    let mut query = r#"
      query Introspection{
        __schema {
           types {
            ...typeFragment1
          }
        }
      }
    "#
    .to_string();

    for i in 1..size {
        query.push_str(&format!(
            "
          fragment typeFragment{i} on __Type {{
            ofType {{
              ...typeFragment{}
            }}
          }}",
            i + 1
        ));
    }
    query.push_str(&format!(
        "
          fragment typeFragment{size} on __Type {{
            ofType {{
              name
            }}
          }}"
    ));

    query
}

fn build_directive_chain(size: usize) -> String {
    let mut schema = r#"
      type Query {
        field: Int! @directive(arg: true)
      }

      directive @directive(arg: Boolean @argDir1) on FIELD_DEFINITION
    "#
    .to_string();

    for i in 1..size {
        schema.push_str(&format!(
            "
            directive @argDir{i}(arg: Boolean @argDir{}) on ARGUMENT_DEFINITION
            ",
            i + 1
        ));
    }
    schema.push_str(&format!(
        "
        directive @argDir{size}(arg: Boolean) on ARGUMENT_DEFINITION
          "
    ));

    schema
}

fn build_input_object_chain(size: usize) -> String {
    let mut schema = r#"
      type Query {
        field(arg: VeryVeryDeep): Boolean
      }

      input VeryVeryDeep {
        nest: VeryVeryDeep1!
      }
    "#
    .to_string();

    for i in 1..size {
        schema.push_str(&format!(
            "
            input VeryVeryDeep{i} {{ nest: VeryVeryDeep{}! }}
            ",
            i + 1
        ));
    }
    schema.push_str(&format!(
        "
        input VeryVeryDeep{size} {{ final: Boolean }}
          "
    ));

    schema
}

/// Returns a deeply nested selection set with a conflict in its leaf fields.
fn build_nested_selection(depth: usize) -> String {
    format!(
        "query {}{}{}",
        "{ recur\n".repeat(depth),
        "{ leaf(arg: true) leaf(arg: false) }",
        "\n}".repeat(depth),
    )
}

#[test]
fn long_fragment_chains_do_not_overflow_stack() {
    // Build a query that applies 1K fragments
    // Validating it would take a lot of recursion and blow the stack
    let query = build_fragment_chain(1_000);

    let errors = apollo_compiler::parse_mixed_validate(
        format!(
            "type Query {{ a: Int }}
            {query}"
        ),
        "overflow.graphql",
    )
    .expect_err("must have recursion errors");

    let expected = expect_test::expect![[r#"
        Error: too much recursion
        Error: too much recursion
        Error: `typeFragment1` contains too much nesting
            ╭─[overflow.graphql:11:11]
            │
         11 │           fragment typeFragment1 on __Type {
            │           ───────────┬──────────  
            │                      ╰──────────── references a very long chain of fragments in its definition
        ────╯
    "#]];
    expected.assert_eq(&errors.to_string());
}

#[test]
fn not_long_enough_fragment_chain_applies_correctly() {
    // Stay just under the recursion limit
    let query = build_fragment_chain(99);

    apollo_compiler::parse_mixed_validate(
        format!(
            "type Query {{ a: Int }}
            {query}"
        ),
        "no_overflow.graphql",
    )
    .expect("must not have recursion errors");
}

#[test]
fn long_directive_chains_do_not_overflow_stack() {
    // Build a schema that defines hundreds of directives that all use each other in their
    // argument list
    // Validating it would take a lot of recursion and a lot of time
    let schema = build_directive_chain(500);

    let partial = apollo_compiler::Schema::parse_and_validate(schema, "directives.graphql")
        .expect_err("must have recursion errors");

    assert_eq!(partial.errors.len(), 469);
}

#[test]
fn not_long_enough_directive_chain_applies_correctly() {
    // Stay just under the recursion limit
    let schema = build_directive_chain(31);

    let _schema = apollo_compiler::Schema::parse_and_validate(schema, "directives.graphql")
        .expect("must not have recursion errors");
}

#[test]
fn long_input_object_chains_do_not_overflow_stack() {
    // Build a very deeply nested input object
    // Validating it would take a lot of recursion and a lot of time
    let schema = build_input_object_chain(500);

    let partial = apollo_compiler::Schema::parse_and_validate(schema, "input_objects.graphql")
        .expect_err("must have recursion errors");

    // The final 199 input objects do not cause recursion errors because the chain is less than 200
    // directives deep.
    assert_eq!(partial.errors.len(), 469);
}

#[test]
fn not_long_enough_input_object_chain_applies_correctly() {
    // Stay just under the recursion limit
    let schema = build_input_object_chain(31);

    let _schema = apollo_compiler::Schema::parse_and_validate(schema, "input_objects.graphql")
        .expect("must not have recursion errors");
}

#[test]
fn deeply_nested_selection_bails_out() {
    // This test relies on implementation details to ensure that the correct recursion limit is
    // tested.
    // The parser has a recursion limit that applies to parsing nested selection sets. So we build
    // a nested selection set that is below this limit.
    // Field merging validation has its own recursion limit that is much lower. We test this limit
    // by writing a conflicting field selection at a nesting level that is too deep for the field
    // merging validation, but not deep enough to cause a parser bailout.
    // If this test fails, it may be due to a change in the hardcoded limits, and removing or
    // replacing this test may be appropriate.

    let schema = r#"
type Recur {
  recur: Recur
  leaf(arg: Boolean): Int
}
type Query {
  recur: Recur
}
    "#;

    let query = build_nested_selection(400);

    let errors =
        apollo_compiler::parse_mixed_validate(format!("{schema}\n{query}"), "selection.graphql")
            .expect_err("must have validation error");

    expect![[r#"
        Error: too much recursion
    "#]]
    .assert_eq(&errors.to_string());
}