api_testing_core/graphql/
runner.rs1use 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}