Skip to main content

api_testing_core/graphql/
runner.rs

1use anyhow::Context;
2
3use crate::Result;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct GraphqlHttpResponse {
7    pub status: u16,
8    pub body: Vec<u8>,
9    pub content_type: Option<String>,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct GraphqlExecutedRequest {
14    pub url: String,
15    pub response: GraphqlHttpResponse,
16}
17
18fn build_payload(operation: &str, variables: Option<&serde_json::Value>) -> serde_json::Value {
19    let mut obj = serde_json::Map::new();
20    obj.insert(
21        "query".to_string(),
22        serde_json::Value::String(operation.to_string()),
23    );
24    if let Some(vars) = variables {
25        obj.insert("variables".to_string(), vars.clone());
26    }
27    serde_json::Value::Object(obj)
28}
29
30pub fn execute_graphql_request(
31    endpoint_url: &str,
32    bearer_token: Option<&str>,
33    operation: &str,
34    variables: Option<&serde_json::Value>,
35) -> Result<GraphqlExecutedRequest> {
36    let payload = build_payload(operation, variables);
37    let bytes =
38        serde_json::to_vec(&payload).context("failed to serialize GraphQL request payload")?;
39
40    let client = reqwest::blocking::Client::new();
41    let mut builder = client
42        .post(endpoint_url)
43        .header(reqwest::header::ACCEPT, "application/json")
44        .header(reqwest::header::CONTENT_TYPE, "application/json")
45        .body(bytes);
46
47    if let Some(token) = bearer_token {
48        builder = builder.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"));
49    }
50
51    let response = builder
52        .send()
53        .with_context(|| format!("HTTP request failed: POST {endpoint_url}"))?;
54
55    let status = response.status().as_u16();
56    let content_type = response
57        .headers()
58        .get(reqwest::header::CONTENT_TYPE)
59        .and_then(|v| v.to_str().ok())
60        .map(|s| s.to_string());
61    let body = response
62        .bytes()
63        .context("failed to read response body")?
64        .to_vec();
65
66    if !(200..300).contains(&status) {
67        anyhow::bail!("HTTP request failed with status {status}.");
68    }
69
70    Ok(GraphqlExecutedRequest {
71        url: endpoint_url.to_string(),
72        response: GraphqlHttpResponse {
73            status,
74            body,
75            content_type,
76        },
77    })
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use nils_test_support::http::{HttpResponse, LoopbackServer};
84
85    #[test]
86    fn graphql_runner_build_payload_includes_vars_only_when_present() {
87        let op = "query { ok }";
88        let with_vars = build_payload(op, Some(&serde_json::json!({"a": 1})));
89        assert!(with_vars.get("variables").is_some());
90        let without_vars = build_payload(op, None);
91        assert!(without_vars.get("variables").is_none());
92    }
93
94    #[test]
95    fn graphql_runner_execute_request_sends_headers_and_body() {
96        let server = LoopbackServer::new().expect("server");
97        server.add_route(
98            "POST",
99            "/graphql",
100            HttpResponse::new(200, r#"{"data":{"ok":true}}"#)
101                .with_header("Content-Type", "application/json"),
102        );
103
104        let endpoint = format!("{}/graphql", server.url());
105        let operation = "query Widget($id: Int!) { widget(id: $id) { id } }";
106        let variables = serde_json::json!({ "id": 7 });
107
108        let executed =
109            execute_graphql_request(&endpoint, Some("token"), operation, Some(&variables))
110                .expect("execute");
111        assert_eq!(executed.url, endpoint);
112        assert_eq!(executed.response.status, 200);
113        assert_eq!(
114            executed.response.content_type.as_deref(),
115            Some("application/json")
116        );
117
118        let requests = server.take_requests();
119        assert_eq!(requests.len(), 1);
120        let req = &requests[0];
121        assert_eq!(req.method, "POST");
122        assert_eq!(req.path, "/graphql");
123        assert_eq!(
124            req.header_value("authorization").as_deref(),
125            Some("Bearer token")
126        );
127        assert_eq!(
128            req.header_value("accept").as_deref(),
129            Some("application/json")
130        );
131        assert_eq!(
132            req.header_value("content-type").as_deref(),
133            Some("application/json")
134        );
135
136        let payload: serde_json::Value =
137            serde_json::from_str(&req.body_text()).expect("request payload");
138        assert_eq!(payload["query"], operation);
139        assert_eq!(payload["variables"], variables);
140    }
141
142    #[test]
143    fn graphql_runner_execute_request_omits_auth_header_when_token_missing() {
144        let server = LoopbackServer::new().expect("server");
145        server.add_route(
146            "POST",
147            "/graphql",
148            HttpResponse::new(200, r#"{"data":{"ok":true}}"#)
149                .with_header("Content-Type", "application/json"),
150        );
151
152        let endpoint = format!("{}/graphql", server.url());
153        execute_graphql_request(&endpoint, None, "query { ok }", None).expect("execute");
154
155        let requests = server.take_requests();
156        assert_eq!(requests.len(), 1);
157        assert!(requests[0].header_value("authorization").is_none());
158    }
159
160    #[test]
161    fn graphql_runner_execute_request_fails_on_non_success_status() {
162        let server = LoopbackServer::new().expect("server");
163        server.add_route(
164            "POST",
165            "/graphql",
166            HttpResponse::new(500, r#"{"error":"boom"}"#)
167                .with_header("Content-Type", "application/json"),
168        );
169
170        let endpoint = format!("{}/graphql", server.url());
171        let err = execute_graphql_request(&endpoint, None, "query { ok }", None).unwrap_err();
172        let msg = format!("{err:#}");
173        assert!(msg.contains("HTTP request failed with status 500."));
174    }
175}