mockforge_recorder/protocols/
graphql.rs

1//! GraphQL recording helpers
2
3use crate::{models::*, recorder::Recorder};
4use chrono::Utc;
5use std::collections::HashMap;
6use tracing::debug;
7use uuid::Uuid;
8
9/// Record a GraphQL query/mutation
10pub async fn record_graphql_request(
11    recorder: &Recorder,
12    operation_type: &str, // "query", "mutation", "subscription"
13    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    // Build request body as GraphQL JSON format
22    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
73/// Record a GraphQL response
74pub 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
106/// Record a GraphQL subscription event
107pub 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        // Verify it was recorded
182        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        // Verify it was recorded
214        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}