mockforge_graphql/
executor.rs

1//! GraphQL execution engine
2
3use async_graphql::http::GraphQLPlaygroundConfig;
4use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
5use axum::{
6    extract::State,
7    response::{Html, IntoResponse},
8    routing::{get, post},
9    Router,
10};
11use mockforge_observability::get_global_registry;
12use std::sync::Arc;
13use std::time::Instant;
14use tokio::net::TcpListener;
15
16use crate::GraphQLSchema;
17
18/// GraphQL executor state
19pub struct GraphQLExecutor {
20    schema: Arc<GraphQLSchema>,
21}
22
23impl GraphQLExecutor {
24    /// Create a new executor
25    pub fn new(schema: GraphQLSchema) -> Self {
26        Self {
27            schema: Arc::new(schema),
28        }
29    }
30
31    /// Execute a GraphQL request
32    pub async fn execute(&self, request: GraphQLRequest) -> GraphQLResponse {
33        let response = self.schema.schema().execute(request.into_inner()).await;
34        response.into()
35    }
36
37    /// Get the schema
38    pub fn schema(&self) -> &GraphQLSchema {
39        &self.schema
40    }
41}
42
43/// Start GraphQL server
44pub async fn start_graphql_server(
45    port: u16,
46    latency_profile: Option<mockforge_core::LatencyProfile>,
47) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
48    let addr = mockforge_core::wildcard_socket_addr(port);
49    tracing::info!("GraphQL server listening on {}", addr);
50
51    let app = create_graphql_router(latency_profile).await?;
52
53    let listener = TcpListener::bind(addr).await?;
54    axum::serve(listener, app).await?;
55
56    Ok(())
57}
58
59/// Create GraphQL router
60pub async fn create_graphql_router(
61    latency_profile: Option<mockforge_core::LatencyProfile>,
62) -> Result<Router, Box<dyn std::error::Error + Send + Sync>> {
63    // Create a basic schema
64    let schema = GraphQLSchema::generate_basic_schema();
65    let executor = GraphQLExecutor::new(schema);
66
67    let mut app = Router::new()
68        .route("/graphql", post(graphql_handler))
69        .route("/graphql", get(graphql_playground))
70        .with_state(Arc::new(executor));
71
72    // Add latency injection if configured
73    if let Some(profile) = latency_profile {
74        let latency_injector =
75            mockforge_core::latency::LatencyInjector::new(profile, Default::default());
76        app = app.layer(axum::middleware::from_fn(
77            move |req: axum::http::Request<axum::body::Body>, next: axum::middleware::Next| {
78                let injector = latency_injector.clone();
79                async move {
80                    let _ = injector.inject_latency(&[]).await;
81                    next.run(req).await
82                }
83            },
84        ));
85    }
86
87    Ok(app)
88}
89
90/// GraphQL endpoint handler
91async fn graphql_handler(
92    State(executor): State<Arc<GraphQLExecutor>>,
93    req: GraphQLRequest,
94) -> GraphQLResponse {
95    let start = Instant::now();
96    let registry = get_global_registry();
97
98    // Track in-flight requests
99    registry.increment_in_flight("graphql");
100
101    // Execute the request
102    let response = executor.execute(req).await;
103
104    // Track completion
105    registry.decrement_in_flight("graphql");
106
107    let duration = start.elapsed().as_secs_f64();
108    let status = if response.0.is_ok() { 200 } else { 400 };
109
110    // Record metrics
111    registry.record_graphql_request("query", status, duration);
112
113    if !response.0.is_ok() {
114        registry.record_error("graphql", "graphql_error");
115    }
116
117    response
118}
119
120/// GraphQL Playground handler
121async fn graphql_playground() -> impl IntoResponse {
122    Html(async_graphql::http::playground_source(
123        GraphQLPlaygroundConfig::new("/graphql")
124            .title("MockForge GraphQL Playground")
125            .subscription_endpoint("/graphql"),
126    ))
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_graphql_executor_new() {
135        let schema = GraphQLSchema::new();
136        let executor = GraphQLExecutor::new(schema);
137
138        // Verify executor is created
139        assert!(!executor.schema.schema().sdl().is_empty());
140    }
141
142    #[test]
143    fn test_graphql_executor_schema_getter() {
144        let schema = GraphQLSchema::new();
145        let executor = GraphQLExecutor::new(schema);
146
147        let retrieved_schema = executor.schema();
148        assert!(!retrieved_schema.schema().sdl().is_empty());
149    }
150
151    #[tokio::test]
152    async fn test_graphql_executor_can_execute() {
153        let schema = GraphQLSchema::new();
154        let executor = GraphQLExecutor::new(schema);
155
156        // Test that we can create an executor and access its schema
157        assert!(executor.schema().schema().sdl().contains("Query"));
158    }
159
160    #[tokio::test]
161    async fn test_create_graphql_router_no_latency() {
162        let result = create_graphql_router(None).await;
163        assert!(result.is_ok());
164    }
165
166    #[tokio::test]
167    async fn test_create_graphql_router_with_latency() {
168        let latency = mockforge_core::LatencyProfile::default();
169        let result = create_graphql_router(Some(latency)).await;
170        assert!(result.is_ok());
171    }
172
173    #[tokio::test]
174    async fn test_create_graphql_router_with_custom_latency() {
175        let latency = mockforge_core::LatencyProfile::new(100, 25);
176        let result = create_graphql_router(Some(latency)).await;
177        assert!(result.is_ok());
178    }
179
180    #[tokio::test]
181    async fn test_graphql_playground_returns_html() {
182        let response = graphql_playground().await;
183        // Convert to response to verify it's valid HTML
184        let _html_response = response.into_response();
185    }
186
187    #[test]
188    fn test_graphql_handler_setup() {
189        let schema = GraphQLSchema::new();
190        let executor = Arc::new(GraphQLExecutor::new(schema));
191
192        // Test that we can create executor and wrap in Arc for handler
193        assert_eq!(Arc::strong_count(&executor), 1);
194    }
195
196    #[test]
197    fn test_executor_arc_shared_ownership() {
198        let schema = GraphQLSchema::new();
199        let executor = Arc::new(GraphQLExecutor::new(schema));
200
201        let executor_clone = Arc::clone(&executor);
202        assert_eq!(Arc::strong_count(&executor), 2);
203
204        drop(executor_clone);
205        assert_eq!(Arc::strong_count(&executor), 1);
206    }
207
208    #[test]
209    fn test_executor_schema_contains_query_type() {
210        let schema = GraphQLSchema::new();
211        let executor = GraphQLExecutor::new(schema);
212
213        let sdl = executor.schema().schema().sdl();
214        assert!(sdl.contains("Query"));
215    }
216
217    #[test]
218    fn test_executor_schema_contains_user_type() {
219        let schema = GraphQLSchema::new();
220        let executor = GraphQLExecutor::new(schema);
221
222        let sdl = executor.schema().schema().sdl();
223        assert!(sdl.contains("User"));
224    }
225
226    #[test]
227    fn test_executor_schema_contains_post_type() {
228        let schema = GraphQLSchema::new();
229        let executor = GraphQLExecutor::new(schema);
230
231        let sdl = executor.schema().schema().sdl();
232        assert!(sdl.contains("Post"));
233    }
234}