async-graphql-test 1.0.0

A test framework for Rust GraphQL servers.
Documentation
use std::future::Future;

use async_graphql::PathSegment;
use itertools::Itertools;

use crate::matchers::MatcherError;
use crate::script::Script;

/// Execute a sequence of GraphQL requests, calling the provided handler on each
/// request.
///
/// The handler can be as simple as `|req| schema.execute(req)`, but you can
/// also put additional logic there.
///
/// Queries and mutations starting with `_` are not returned in the final list
/// of responses. This is useful for ommiting the feedback from the initial
/// "populate the DB" boilerplate in the response snapshot.
pub async fn execute_script<F, Fut>(
    mut execute_request: F,
    script: Script,
) -> Vec<async_graphql::Response>
where
    F: FnMut(async_graphql::Request) -> Fut,
    Fut: Future<Output = async_graphql::Response>,
{
    let requests = script.requests;
    let mut acc = Vec::with_capacity(requests.len());

    if requests.is_empty() {
        panic!("at least one query or mutation must be present");
    }

    for mut req in requests {
        // reset operation_name, otherwise an error is triggered on execution
        // https://github.com/async-graphql/async-graphql/blob/df11cfe35fa0762dadfd86a2cbe1f084d3061469/src/schema.rs#L839
        let op = req
            .parsed_query()
            .unwrap()
            .operations
            .iter()
            .next()
            .unwrap()
            .1
            .clone();
        let op_type = op.node.ty;

        let op_name = req.operation_name.take().unwrap_or_default();
        let failure_causes_panic = op_name.starts_with('_');

        let res = execute_request(req).await;

        let has_matcher_err = res
            .errors
            .iter()
            .any(|err| err.source::<MatcherError>().is_some());

        if res.is_err() && (has_matcher_err || failure_causes_panic) {
            // we'll print only the first error and exit
            if let Some(err) = res.errors.first() {
                // err formating and the context around must be moved to the MatcherError
                let path = GraphQLPathSegments(&err.path);
                let source = err.source.as_deref();
                // printing the error like this is more readable than printing it in the `panic!`
                eprintln!(
                    "{:?} '{}' ({}) raised an error at line {}\n\n{}\n\nSource:\n{:?}\n",
                    op_type, op_name, path, err.locations[0], err.message, source,
                );
                // abort execution after we've printed the error
                panic!();
            }
        }

        if !failure_causes_panic || acc.len() == 1 {
            acc.push(res);
        }
    }

    acc
}

struct GraphQLPathSegments<'a>(&'a [PathSegment]);

impl std::fmt::Display for GraphQLPathSegments<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0
            .iter()
            .format_with(".", |segment, f| match segment {
                PathSegment::Field(name) => f(name),
                PathSegment::Index(idx) => f(&format_args!("[{idx}]")),
            })
            .fmt(f)
    }
}