nils-api-testing-core 1.21.9

Library crate for nils-api-testing-core in the nils-cli workspace.
Documentation
use anyhow::Context;

use crate::Result;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphqlHttpResponse {
    pub status: u16,
    pub body: Vec<u8>,
    pub content_type: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphqlExecutedRequest {
    pub url: String,
    pub response: GraphqlHttpResponse,
}

fn build_payload(operation: &str, variables: Option<&serde_json::Value>) -> serde_json::Value {
    let mut obj = serde_json::Map::new();
    obj.insert(
        "query".to_string(),
        serde_json::Value::String(operation.to_string()),
    );
    if let Some(vars) = variables {
        obj.insert("variables".to_string(), vars.clone());
    }
    serde_json::Value::Object(obj)
}

pub fn execute_graphql_request(
    endpoint_url: &str,
    bearer_token: Option<&str>,
    operation: &str,
    variables: Option<&serde_json::Value>,
) -> Result<GraphqlExecutedRequest> {
    let payload = build_payload(operation, variables);
    let bytes =
        serde_json::to_vec(&payload).context("failed to serialize GraphQL request payload")?;

    let client = reqwest::blocking::Client::new();
    let mut builder = client
        .post(endpoint_url)
        .header(reqwest::header::ACCEPT, "application/json")
        .header(reqwest::header::CONTENT_TYPE, "application/json")
        .body(bytes);

    if let Some(token) = bearer_token {
        builder = builder.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"));
    }

    let response = builder
        .send()
        .with_context(|| format!("HTTP request failed: POST {endpoint_url}"))?;

    let status = response.status().as_u16();
    let content_type = response
        .headers()
        .get(reqwest::header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string());
    let body = response
        .bytes()
        .context("failed to read response body")?
        .to_vec();

    if !(200..300).contains(&status) {
        anyhow::bail!("HTTP request failed with status {status}.");
    }

    Ok(GraphqlExecutedRequest {
        url: endpoint_url.to_string(),
        response: GraphqlHttpResponse {
            status,
            body,
            content_type,
        },
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use nils_test_support::http::{HttpResponse, LoopbackServer};

    #[test]
    fn graphql_runner_build_payload_includes_vars_only_when_present() {
        let op = "query { ok }";
        let with_vars = build_payload(op, Some(&serde_json::json!({"a": 1})));
        assert!(with_vars.get("variables").is_some());
        let without_vars = build_payload(op, None);
        assert!(without_vars.get("variables").is_none());
    }

    #[test]
    fn graphql_runner_execute_request_sends_headers_and_body() {
        let server = LoopbackServer::new().expect("server");
        server.add_route(
            "POST",
            "/graphql",
            HttpResponse::new(200, r#"{"data":{"ok":true}}"#)
                .with_header("Content-Type", "application/json"),
        );

        let endpoint = format!("{}/graphql", server.url());
        let operation = "query Widget($id: Int!) { widget(id: $id) { id } }";
        let variables = serde_json::json!({ "id": 7 });

        let executed =
            execute_graphql_request(&endpoint, Some("token"), operation, Some(&variables))
                .expect("execute");
        assert_eq!(executed.url, endpoint);
        assert_eq!(executed.response.status, 200);
        assert_eq!(
            executed.response.content_type.as_deref(),
            Some("application/json")
        );

        let requests = server.take_requests();
        assert_eq!(requests.len(), 1);
        let req = &requests[0];
        assert_eq!(req.method, "POST");
        assert_eq!(req.path, "/graphql");
        assert_eq!(
            req.header_value("authorization").as_deref(),
            Some("Bearer token")
        );
        assert_eq!(
            req.header_value("accept").as_deref(),
            Some("application/json")
        );
        assert_eq!(
            req.header_value("content-type").as_deref(),
            Some("application/json")
        );

        let payload: serde_json::Value =
            serde_json::from_str(&req.body_text()).expect("request payload");
        assert_eq!(payload["query"], operation);
        assert_eq!(payload["variables"], variables);
    }

    #[test]
    fn graphql_runner_execute_request_omits_auth_header_when_token_missing() {
        let server = LoopbackServer::new().expect("server");
        server.add_route(
            "POST",
            "/graphql",
            HttpResponse::new(200, r#"{"data":{"ok":true}}"#)
                .with_header("Content-Type", "application/json"),
        );

        let endpoint = format!("{}/graphql", server.url());
        execute_graphql_request(&endpoint, None, "query { ok }", None).expect("execute");

        let requests = server.take_requests();
        assert_eq!(requests.len(), 1);
        assert!(requests[0].header_value("authorization").is_none());
    }

    #[test]
    fn graphql_runner_execute_request_fails_on_non_success_status() {
        let server = LoopbackServer::new().expect("server");
        server.add_route(
            "POST",
            "/graphql",
            HttpResponse::new(500, r#"{"error":"boom"}"#)
                .with_header("Content-Type", "application/json"),
        );

        let endpoint = format!("{}/graphql", server.url());
        let err = execute_graphql_request(&endpoint, None, "query { ok }", None).unwrap_err();
        let msg = format!("{err:#}");
        assert!(msg.contains("HTTP request failed with status 500."));
    }
}