Skip to main content

mockforge_graphql/
executor.rs

1//! GraphQL execution engine
2
3use async_graphql::http::GraphQLPlaygroundConfig;
4use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
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_foundation::latency::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_foundation::latency::LatencyProfile>,
62) -> Result<Router, Box<dyn std::error::Error + Send + Sync>> {
63    // Create a basic schema
64    let schema = GraphQLSchema::generate_basic_schema();
65    // `async-graphql-axum`'s `GraphQLSubscription::new` wants the
66    // schema directly (not wrapped in our executor), so clone the
67    // inner async-graphql Schema for the /graphql/ws route before
68    // moving `schema` into the executor.
69    let schema_for_subscriptions = schema.schema().clone();
70    let executor = GraphQLExecutor::new(schema);
71
72    let mut app = Router::new()
73        .route("/graphql", post(graphql_handler))
74        .route("/graphql", get(graphql_playground))
75        // WebSocket endpoint for GraphQL subscriptions using the
76        // graphql-transport-ws protocol (the default in both
77        // async-graphql-axum and Apollo Sandbox). Clients that only
78        // support the older graphql-ws still work because the axum
79        // handler negotiates both subprotocols. `GraphQLSubscription`
80        // is a `tower::Service`, so we use `route_service` rather
81        // than `route` + a handler function.
82        .route_service("/graphql/ws", GraphQLSubscription::new(schema_for_subscriptions))
83        .with_state(Arc::new(executor));
84
85    // Add latency injection if configured
86    if let Some(profile) = latency_profile {
87        let latency_injector =
88            mockforge_foundation::latency::LatencyInjector::new(profile, Default::default());
89        app = app.layer(axum::middleware::from_fn(
90            move |req: axum::http::Request<axum::body::Body>, next: axum::middleware::Next| {
91                let injector = latency_injector.clone();
92                async move {
93                    let _ = injector.inject_latency(&[]).await;
94                    next.run(req).await
95                }
96            },
97        ));
98    }
99
100    Ok(app)
101}
102
103/// GraphQL endpoint handler
104async fn graphql_handler(
105    State(executor): State<Arc<GraphQLExecutor>>,
106    req: GraphQLRequest,
107) -> GraphQLResponse {
108    let start = Instant::now();
109    let registry = get_global_registry();
110
111    // Track in-flight requests
112    registry.increment_in_flight("graphql");
113
114    // Execute the request
115    let response = executor.execute(req).await;
116
117    // Track completion
118    registry.decrement_in_flight("graphql");
119
120    let duration = start.elapsed().as_secs_f64();
121    let status = if response.0.is_ok() { 200 } else { 400 };
122
123    // Record metrics
124    registry.record_graphql_request("query", status, duration);
125
126    if !response.0.is_ok() {
127        registry.record_error("graphql", "graphql_error");
128    }
129
130    response
131}
132
133/// GraphQL Playground handler
134async fn graphql_playground() -> impl IntoResponse {
135    Html(async_graphql::http::playground_source(
136        GraphQLPlaygroundConfig::new("/graphql")
137            .title("MockForge GraphQL Playground")
138            .subscription_endpoint("/graphql/ws"),
139    ))
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_graphql_executor_new() {
148        let schema = GraphQLSchema::new();
149        let executor = GraphQLExecutor::new(schema);
150
151        // Verify executor is created
152        assert!(!executor.schema.schema().sdl().is_empty());
153    }
154
155    #[test]
156    fn test_graphql_executor_schema_getter() {
157        let schema = GraphQLSchema::new();
158        let executor = GraphQLExecutor::new(schema);
159
160        let retrieved_schema = executor.schema();
161        assert!(!retrieved_schema.schema().sdl().is_empty());
162    }
163
164    #[tokio::test]
165    async fn test_graphql_executor_can_execute() {
166        let schema = GraphQLSchema::new();
167        let executor = GraphQLExecutor::new(schema);
168
169        // Test that we can create an executor and access its schema
170        assert!(executor.schema().schema().sdl().contains("Query"));
171    }
172
173    #[tokio::test]
174    async fn test_create_graphql_router_no_latency() {
175        let result = create_graphql_router(None).await;
176        assert!(result.is_ok());
177    }
178
179    #[tokio::test]
180    async fn test_create_graphql_router_with_latency() {
181        let latency = mockforge_foundation::latency::LatencyProfile::default();
182        let result = create_graphql_router(Some(latency)).await;
183        assert!(result.is_ok());
184    }
185
186    #[tokio::test]
187    async fn test_create_graphql_router_with_custom_latency() {
188        let latency = mockforge_foundation::latency::LatencyProfile::new(100, 25);
189        let result = create_graphql_router(Some(latency)).await;
190        assert!(result.is_ok());
191    }
192
193    #[tokio::test]
194    async fn test_graphql_playground_returns_html() {
195        let response = graphql_playground().await;
196        // Convert to response to verify it's valid HTML
197        let _html_response = response.into_response();
198    }
199
200    #[test]
201    fn test_graphql_handler_setup() {
202        let schema = GraphQLSchema::new();
203        let executor = Arc::new(GraphQLExecutor::new(schema));
204
205        // Test that we can create executor and wrap in Arc for handler
206        assert_eq!(Arc::strong_count(&executor), 1);
207    }
208
209    #[test]
210    fn test_executor_arc_shared_ownership() {
211        let schema = GraphQLSchema::new();
212        let executor = Arc::new(GraphQLExecutor::new(schema));
213
214        let executor_clone = Arc::clone(&executor);
215        assert_eq!(Arc::strong_count(&executor), 2);
216
217        drop(executor_clone);
218        assert_eq!(Arc::strong_count(&executor), 1);
219    }
220
221    #[test]
222    fn test_executor_schema_contains_query_type() {
223        let schema = GraphQLSchema::new();
224        let executor = GraphQLExecutor::new(schema);
225
226        let sdl = executor.schema().schema().sdl();
227        assert!(sdl.contains("Query"));
228    }
229
230    #[test]
231    fn test_executor_schema_contains_user_type() {
232        let schema = GraphQLSchema::new();
233        let executor = GraphQLExecutor::new(schema);
234
235        let sdl = executor.schema().schema().sdl();
236        assert!(sdl.contains("User"));
237    }
238
239    #[test]
240    fn test_executor_schema_contains_post_type() {
241        let schema = GraphQLSchema::new();
242        let executor = GraphQLExecutor::new(schema);
243
244        let sdl = executor.schema().schema().sdl();
245        assert!(sdl.contains("Post"));
246    }
247}