mockforge_recorder/protocols/
graphql.rs1use crate::{models::*, recorder::Recorder};
4use chrono::Utc;
5use std::collections::HashMap;
6use tracing::debug;
7use uuid::Uuid;
8
9pub async fn record_graphql_request(
11 recorder: &Recorder,
12 operation_type: &str, operation_name: Option<&str>,
14 query: &str,
15 variables: Option<&str>,
16 headers: &HashMap<String, String>,
17 context: &crate::models::RequestContext,
18) -> Result<String, crate::RecorderError> {
19 let request_id = Uuid::new_v4().to_string();
20
21 let mut body_json = serde_json::json!({
23 "query": query,
24 });
25
26 if let Some(vars) = variables {
27 if let Ok(vars_json) = serde_json::from_str::<serde_json::Value>(vars) {
28 body_json["variables"] = vars_json;
29 }
30 }
31
32 if let Some(name) = operation_name {
33 body_json["operationName"] = serde_json::json!(name);
34 }
35
36 let body_str = serde_json::to_string(&body_json)?;
37 let method = format!("GraphQL {}", operation_type.to_uppercase());
38
39 let mut tags = vec!["graphql".to_string(), operation_type.to_string()];
40 if let Some(name) = operation_name {
41 tags.push(name.to_string());
42 }
43
44 let request = RecordedRequest {
45 id: request_id.clone(),
46 protocol: Protocol::GraphQL,
47 timestamp: Utc::now(),
48 method,
49 path: "/graphql".to_string(),
50 query_params: None,
51 headers: serde_json::to_string(&headers)?,
52 body: Some(body_str),
53 body_encoding: "utf8".to_string(),
54 client_ip: context.client_ip.clone(),
55 trace_id: context.trace_id.clone(),
56 span_id: context.span_id.clone(),
57 duration_ms: None,
58 status_code: None,
59 tags: Some(serde_json::to_string(&tags)?),
60 };
61
62 recorder.record_request(request).await?;
63 debug!(
64 "Recorded GraphQL request: {} {} {}",
65 request_id,
66 operation_type,
67 operation_name.unwrap_or("anonymous")
68 );
69
70 Ok(request_id)
71}
72
73pub async fn record_graphql_response(
75 recorder: &Recorder,
76 request_id: &str,
77 response_json: &str,
78 has_errors: bool,
79 duration_ms: i64,
80) -> Result<(), crate::RecorderError> {
81 let status_code = if has_errors { 400 } else { 200 };
82 let size_bytes = response_json.len() as i64;
83
84 let response = RecordedResponse {
85 request_id: request_id.to_string(),
86 status_code,
87 headers: serde_json::to_string(&HashMap::from([(
88 "content-type".to_string(),
89 "application/json".to_string(),
90 )]))?,
91 body: Some(response_json.to_string()),
92 body_encoding: "utf8".to_string(),
93 size_bytes,
94 timestamp: Utc::now(),
95 };
96
97 recorder.record_response(response).await?;
98 debug!(
99 "Recorded GraphQL response: {} status={} duration={}ms",
100 request_id, status_code, duration_ms
101 );
102
103 Ok(())
104}
105
106pub async fn record_graphql_subscription_event(
108 recorder: &Recorder,
109 subscription_id: &str,
110 event_data: &str,
111 trace_id: Option<&str>,
112 span_id: Option<&str>,
113) -> Result<String, crate::RecorderError> {
114 let event_id = Uuid::new_v4().to_string();
115
116 let request = RecordedRequest {
117 id: event_id.clone(),
118 protocol: Protocol::GraphQL,
119 timestamp: Utc::now(),
120 method: "GraphQL SUBSCRIPTION_EVENT".to_string(),
121 path: format!("/graphql/subscriptions/{}", subscription_id),
122 query_params: None,
123 headers: serde_json::to_string(&HashMap::from([(
124 "subscription-id".to_string(),
125 subscription_id.to_string(),
126 )]))?,
127 body: Some(event_data.to_string()),
128 body_encoding: "utf8".to_string(),
129 client_ip: None,
130 trace_id: trace_id.map(|s| s.to_string()),
131 span_id: span_id.map(|s| s.to_string()),
132 duration_ms: None,
133 status_code: Some(200),
134 tags: Some(serde_json::to_string(&vec!["graphql", "subscription", "event"])?),
135 };
136
137 recorder.record_request(request).await?;
138 debug!(
139 "Recorded GraphQL subscription event: {} subscription={}",
140 event_id, subscription_id
141 );
142
143 Ok(event_id)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::database::RecorderDatabase;
150
151 #[tokio::test]
152 async fn test_record_graphql_query() {
153 let db = RecorderDatabase::new_in_memory().await.unwrap();
154 let recorder = Recorder::new(db);
155
156 let headers = HashMap::from([("content-type".to_string(), "application/json".to_string())]);
157
158 let query = "query GetUser($id: ID!) { user(id: $id) { id name email } }";
159 let variables = r#"{"id": "123"}"#;
160
161 let context = crate::models::RequestContext::new(Some("127.0.0.1"), None, None);
162 let request_id = record_graphql_request(
163 &recorder,
164 "query",
165 Some("GetUser"),
166 query,
167 Some(variables),
168 &headers,
169 &context,
170 )
171 .await
172 .unwrap();
173
174 let response_json =
175 r#"{"data": {"user": {"id": "123", "name": "John", "email": "john@example.com"}}}"#;
176
177 record_graphql_response(&recorder, &request_id, response_json, false, 42)
178 .await
179 .unwrap();
180
181 let exchange = recorder.database().get_exchange(&request_id).await.unwrap();
183 assert!(exchange.is_some());
184
185 let exchange = exchange.unwrap();
186 assert_eq!(exchange.request.protocol, Protocol::GraphQL);
187 assert_eq!(exchange.request.method, "GraphQL QUERY");
188 }
189
190 #[tokio::test]
191 async fn test_record_graphql_mutation() {
192 let db = RecorderDatabase::new_in_memory().await.unwrap();
193 let recorder = Recorder::new(db);
194
195 let headers = HashMap::from([("content-type".to_string(), "application/json".to_string())]);
196
197 let mutation =
198 "mutation CreateUser($input: UserInput!) { createUser(input: $input) { id name } }";
199
200 let context = crate::models::RequestContext::new(Some("127.0.0.1"), None, None);
201 let request_id = record_graphql_request(
202 &recorder,
203 "mutation",
204 Some("CreateUser"),
205 mutation,
206 None,
207 &headers,
208 &context,
209 )
210 .await
211 .unwrap();
212
213 let exchange = recorder.database().get_exchange(&request_id).await.unwrap();
215 assert!(exchange.is_some());
216
217 let exchange = exchange.unwrap();
218 assert_eq!(exchange.request.protocol, Protocol::GraphQL);
219 assert_eq!(exchange.request.method, "GraphQL MUTATION");
220 }
221}