mockforge_ui/handlers/
graph.rs

1//! Graph visualization handlers
2//!
3//! These handlers provide graph data for visualizing mock environments,
4//! endpoints, their relationships, and state transitions.
5
6use axum::{
7    extract::State,
8    http::StatusCode,
9    response::{
10        sse::{Event, Sse},
11        IntoResponse, Json,
12    },
13};
14use futures_util::stream::{self, Stream};
15use mockforge_core::graph::GraphBuilder;
16use mockforge_core::request_chaining::ChainDefinition;
17use serde_json::Value;
18use std::convert::Infallible;
19use std::time::Duration;
20
21use super::AdminState;
22use crate::models::ApiResponse;
23
24/// Get graph data for visualization
25///
26/// This endpoint aggregates data from multiple sources:
27/// - Endpoints from UI Builder API
28/// - Request chains
29/// - State machines (if available)
30/// - Workspaces/services
31pub async fn get_graph(State(state): State<AdminState>) -> impl IntoResponse {
32    let mut builder = GraphBuilder::new();
33
34    // Fetch chains from the HTTP server
35    if let Some(http_addr) = state.http_server_addr {
36        match fetch_chains_from_server(http_addr).await {
37            Ok(chains) => {
38                builder.from_chains(&chains);
39            }
40            Err(e) => {
41                tracing::warn!("Failed to fetch chains for graph: {}", e);
42                // Continue without chains - graph will still work
43            }
44        }
45    }
46
47    // Fetch endpoints from UI Builder API if available
48    if let Some(http_addr) = state.http_server_addr {
49        match fetch_endpoints_from_ui_builder(http_addr).await {
50            Ok(endpoints) => {
51                // Convert UI Builder endpoints to graph format
52                for endpoint in endpoints {
53                    let protocol_str = match endpoint.protocol {
54                        mockforge_http::ui_builder::Protocol::Http => "http",
55                        mockforge_http::ui_builder::Protocol::Grpc => "grpc",
56                        mockforge_http::ui_builder::Protocol::Websocket => "websocket",
57                        mockforge_http::ui_builder::Protocol::Graphql => "graphql",
58                        mockforge_http::ui_builder::Protocol::Mqtt => "mqtt",
59                        mockforge_http::ui_builder::Protocol::Smtp => "smtp",
60                        mockforge_http::ui_builder::Protocol::Kafka => "kafka",
61                        mockforge_http::ui_builder::Protocol::Amqp => "amqp",
62                        mockforge_http::ui_builder::Protocol::Ftp => "ftp",
63                    };
64
65                    let mut metadata = std::collections::HashMap::new();
66                    metadata.insert("enabled".to_string(), Value::Bool(endpoint.enabled));
67                    if let Some(desc) = endpoint.description {
68                        metadata.insert("description".to_string(), Value::String(desc));
69                    }
70
71                    // Extract method and path from HTTP config if available
72                    if let mockforge_http::ui_builder::EndpointProtocolConfig::Http(http_config) =
73                        &endpoint.config
74                    {
75                        metadata.insert(
76                            "method".to_string(),
77                            Value::String(http_config.method.clone()),
78                        );
79                        metadata
80                            .insert("path".to_string(), Value::String(http_config.path.clone()));
81                    }
82
83                    let protocol = match protocol_str {
84                        "http" => mockforge_core::graph::Protocol::Http,
85                        "grpc" => mockforge_core::graph::Protocol::Grpc,
86                        "websocket" => mockforge_core::graph::Protocol::Websocket,
87                        "graphql" => mockforge_core::graph::Protocol::Graphql,
88                        "mqtt" => mockforge_core::graph::Protocol::Mqtt,
89                        "smtp" => mockforge_core::graph::Protocol::Smtp,
90                        "kafka" => mockforge_core::graph::Protocol::Kafka,
91                        "amqp" => mockforge_core::graph::Protocol::Amqp,
92                        "ftp" => mockforge_core::graph::Protocol::Ftp,
93                        _ => mockforge_core::graph::Protocol::Http,
94                    };
95
96                    builder.add_endpoint(endpoint.id, endpoint.name, protocol, metadata);
97                }
98            }
99            Err(e) => {
100                tracing::debug!("UI Builder endpoints not available: {}", e);
101                // Continue without endpoints - graph will still work with chains
102            }
103        }
104    }
105
106    // Build the graph
107    let graph_data = builder.build();
108
109    Json(ApiResponse::success(graph_data))
110}
111
112/// Fetch endpoints from UI Builder API
113async fn fetch_endpoints_from_ui_builder(
114    http_addr: std::net::SocketAddr,
115) -> Result<Vec<mockforge_http::ui_builder::EndpointConfig>, String> {
116    let url = format!("http://{}/__mockforge/ui-builder/endpoints", http_addr);
117    let client = reqwest::Client::new();
118
119    let response = client
120        .get(&url)
121        .send()
122        .await
123        .map_err(|e| format!("Failed to fetch endpoints: {}", e))?;
124
125    if !response.status().is_success() {
126        return Err(format!("HTTP error: {}", response.status()));
127    }
128
129    let json: Value =
130        response.json().await.map_err(|e| format!("Failed to parse response: {}", e))?;
131
132    // Extract endpoints from response
133    // Assuming it returns: { "endpoints": [...] } or { "data": { "endpoints": [...] } }
134    let endpoints_array = json
135        .get("endpoints")
136        .or_else(|| json.get("data").and_then(|d| d.get("endpoints")))
137        .and_then(|v| v.as_array())
138        .ok_or_else(|| "Invalid response format: endpoints array not found".to_string())?;
139
140    let mut endpoints = Vec::new();
141    for endpoint_value in endpoints_array {
142        if let Ok(endpoint) = serde_json::from_value::<mockforge_http::ui_builder::EndpointConfig>(
143            endpoint_value.clone(),
144        ) {
145            endpoints.push(endpoint);
146        }
147    }
148
149    Ok(endpoints)
150}
151
152/// SSE endpoint for real-time graph updates
153pub async fn graph_sse(
154    State(state): State<AdminState>,
155) -> Sse<impl Stream<Item = std::result::Result<Event, Infallible>>> {
156    tracing::info!("SSE endpoint /graph/sse accessed - starting real-time graph updates");
157
158    // Clone state for use in the stream
159    let http_addr = state.http_server_addr;
160
161    let stream = stream::unfold((), move |_| {
162        let http_addr = http_addr;
163        async move {
164            tokio::time::sleep(Duration::from_secs(5)).await; // Update every 5 seconds
165
166            // Build graph data (same logic as get_graph)
167            let mut builder = GraphBuilder::new();
168
169            // Fetch chains
170            if let Some(addr) = http_addr {
171                if let Ok(chains) = fetch_chains_from_server(addr).await {
172                    builder.from_chains(&chains);
173                }
174
175                // Fetch endpoints from UI Builder
176                if let Ok(endpoints) = fetch_endpoints_from_ui_builder(addr).await {
177                    for endpoint in endpoints {
178                        let protocol_str = match endpoint.protocol {
179                            mockforge_http::ui_builder::Protocol::Http => "http",
180                            mockforge_http::ui_builder::Protocol::Grpc => "grpc",
181                            mockforge_http::ui_builder::Protocol::Websocket => "websocket",
182                            mockforge_http::ui_builder::Protocol::Graphql => "graphql",
183                            mockforge_http::ui_builder::Protocol::Mqtt => "mqtt",
184                            mockforge_http::ui_builder::Protocol::Smtp => "smtp",
185                            mockforge_http::ui_builder::Protocol::Kafka => "kafka",
186                            mockforge_http::ui_builder::Protocol::Amqp => "amqp",
187                            mockforge_http::ui_builder::Protocol::Ftp => "ftp",
188                        };
189
190                        let mut metadata = std::collections::HashMap::new();
191                        metadata.insert("enabled".to_string(), Value::Bool(endpoint.enabled));
192                        if let Some(desc) = endpoint.description {
193                            metadata.insert("description".to_string(), Value::String(desc));
194                        }
195
196                        if let mockforge_http::ui_builder::EndpointProtocolConfig::Http(
197                            http_config,
198                        ) = &endpoint.config
199                        {
200                            metadata.insert(
201                                "method".to_string(),
202                                Value::String(http_config.method.clone()),
203                            );
204                            metadata.insert(
205                                "path".to_string(),
206                                Value::String(http_config.path.clone()),
207                            );
208                        }
209
210                        let protocol = match protocol_str {
211                            "http" => mockforge_core::graph::Protocol::Http,
212                            "grpc" => mockforge_core::graph::Protocol::Grpc,
213                            "websocket" => mockforge_core::graph::Protocol::Websocket,
214                            "graphql" => mockforge_core::graph::Protocol::Graphql,
215                            "mqtt" => mockforge_core::graph::Protocol::Mqtt,
216                            "smtp" => mockforge_core::graph::Protocol::Smtp,
217                            "kafka" => mockforge_core::graph::Protocol::Kafka,
218                            "amqp" => mockforge_core::graph::Protocol::Amqp,
219                            "ftp" => mockforge_core::graph::Protocol::Ftp,
220                            _ => mockforge_core::graph::Protocol::Http,
221                        };
222
223                        builder.add_endpoint(endpoint.id, endpoint.name, protocol, metadata);
224                    }
225                }
226            }
227
228            let graph_data = builder.build();
229            let json_data = serde_json::to_string(&graph_data).unwrap_or_default();
230
231            Some((Ok(Event::default().data(json_data)), ()))
232        }
233    });
234
235    Sse::new(stream).keep_alive(
236        axum::response::sse::KeepAlive::new()
237            .interval(Duration::from_secs(15))
238            .text("keep-alive-text"),
239    )
240}
241
242/// Fetch chains from the HTTP server
243async fn fetch_chains_from_server(
244    http_addr: std::net::SocketAddr,
245) -> Result<Vec<ChainDefinition>, String> {
246    let url = format!("http://{}/__mockforge/chains", http_addr);
247    let client = reqwest::Client::new();
248
249    let response = client
250        .get(&url)
251        .send()
252        .await
253        .map_err(|e| format!("Failed to fetch chains: {}", e))?;
254
255    if !response.status().is_success() {
256        return Err(format!("HTTP error: {}", response.status()));
257    }
258
259    let json: Value =
260        response.json().await.map_err(|e| format!("Failed to parse response: {}", e))?;
261
262    // Extract chains from the response
263    // The response format depends on the chain API implementation
264    // Assuming it returns: { "chains": [...] } or { "data": { "chains": [...] } }
265    let chains_array = json
266        .get("chains")
267        .or_else(|| json.get("data").and_then(|d| d.get("chains")))
268        .and_then(|v| v.as_array())
269        .ok_or_else(|| "Invalid response format: chains array not found".to_string())?;
270
271    let mut chains = Vec::new();
272    for chain_value in chains_array {
273        // Try to get the full chain definition
274        // First try to get by ID and fetch full details
275        if let Some(chain_id) = chain_value.get("id").and_then(|v| v.as_str()) {
276            match fetch_chain_details(http_addr, chain_id).await {
277                Ok(Some(chain)) => chains.push(chain),
278                Ok(None) => {
279                    // Chain not found, skip
280                    tracing::warn!("Chain {} not found, skipping", chain_id);
281                }
282                Err(e) => {
283                    tracing::warn!("Failed to fetch chain {}: {}", chain_id, e);
284                    // Try to parse from summary if available
285                    if let Ok(chain) =
286                        serde_json::from_value::<ChainDefinition>(chain_value.clone())
287                    {
288                        chains.push(chain);
289                    }
290                }
291            }
292        }
293    }
294
295    Ok(chains)
296}
297
298/// Fetch full chain details by ID
299async fn fetch_chain_details(
300    http_addr: std::net::SocketAddr,
301    chain_id: &str,
302) -> Result<Option<ChainDefinition>, String> {
303    let url = format!("http://{}/__mockforge/chains/{}", http_addr, chain_id);
304    let client = reqwest::Client::new();
305
306    let response = client
307        .get(&url)
308        .send()
309        .await
310        .map_err(|e| format!("Failed to fetch chain details: {}", e))?;
311
312    if response.status() == StatusCode::NOT_FOUND {
313        return Ok(None);
314    }
315
316    if !response.status().is_success() {
317        return Err(format!("HTTP error: {}", response.status()));
318    }
319
320    let json: Value =
321        response.json().await.map_err(|e| format!("Failed to parse response: {}", e))?;
322
323    // Extract chain from response
324    // Assuming it returns: { "chain": {...} } or { "data": {...} } or just the chain object
325    let chain_value = json.get("chain").or_else(|| json.get("data")).unwrap_or(&json);
326
327    serde_json::from_value::<ChainDefinition>(chain_value.clone())
328        .map(Some)
329        .map_err(|e| format!("Failed to deserialize chain: {}", e))
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use mockforge_core::graph::Protocol;
336
337    // ==================== GraphBuilder Tests ====================
338
339    #[test]
340    fn test_graph_builder_creation() {
341        let builder = GraphBuilder::new();
342        let graph = builder.build();
343        assert_eq!(graph.nodes.len(), 0);
344        assert_eq!(graph.edges.len(), 0);
345        assert_eq!(graph.clusters.len(), 0);
346    }
347
348    #[test]
349    fn test_graph_builder_add_endpoint() {
350        let mut builder = GraphBuilder::new();
351        let mut metadata = std::collections::HashMap::new();
352        metadata.insert("enabled".to_string(), Value::Bool(true));
353
354        builder.add_endpoint(
355            "endpoint-1".to_string(),
356            "Test Endpoint".to_string(),
357            Protocol::Http,
358            metadata,
359        );
360
361        let graph = builder.build();
362        assert_eq!(graph.nodes.len(), 1);
363    }
364
365    #[test]
366    fn test_graph_builder_multiple_endpoints() {
367        let mut builder = GraphBuilder::new();
368
369        for i in 0..5 {
370            let metadata = std::collections::HashMap::new();
371            builder.add_endpoint(
372                format!("endpoint-{}", i),
373                format!("Endpoint {}", i),
374                Protocol::Http,
375                metadata,
376            );
377        }
378
379        let graph = builder.build();
380        assert_eq!(graph.nodes.len(), 5);
381    }
382
383    #[test]
384    fn test_graph_builder_different_protocols() {
385        let mut builder = GraphBuilder::new();
386        let metadata = std::collections::HashMap::new();
387
388        let protocols = vec![
389            Protocol::Http,
390            Protocol::Grpc,
391            Protocol::Websocket,
392            Protocol::Graphql,
393            Protocol::Mqtt,
394        ];
395
396        for (i, protocol) in protocols.into_iter().enumerate() {
397            builder.add_endpoint(
398                format!("endpoint-{}", i),
399                format!("Protocol {}", i),
400                protocol,
401                metadata.clone(),
402            );
403        }
404
405        let graph = builder.build();
406        assert_eq!(graph.nodes.len(), 5);
407    }
408
409    #[test]
410    fn test_graph_builder_with_metadata() {
411        let mut builder = GraphBuilder::new();
412        let mut metadata = std::collections::HashMap::new();
413        metadata.insert("method".to_string(), Value::String("GET".to_string()));
414        metadata.insert("path".to_string(), Value::String("/api/users".to_string()));
415        metadata.insert("enabled".to_string(), Value::Bool(true));
416
417        builder.add_endpoint(
418            "http-endpoint".to_string(),
419            "HTTP API".to_string(),
420            Protocol::Http,
421            metadata,
422        );
423
424        let graph = builder.build();
425        assert_eq!(graph.nodes.len(), 1);
426    }
427
428    // ==================== Protocol Conversion Tests ====================
429
430    #[test]
431    fn test_protocol_http() {
432        let protocol = Protocol::Http;
433        assert!(matches!(protocol, Protocol::Http));
434    }
435
436    #[test]
437    fn test_protocol_grpc() {
438        let protocol = Protocol::Grpc;
439        assert!(matches!(protocol, Protocol::Grpc));
440    }
441
442    #[test]
443    fn test_protocol_websocket() {
444        let protocol = Protocol::Websocket;
445        assert!(matches!(protocol, Protocol::Websocket));
446    }
447
448    #[test]
449    fn test_protocol_graphql() {
450        let protocol = Protocol::Graphql;
451        assert!(matches!(protocol, Protocol::Graphql));
452    }
453
454    #[test]
455    fn test_protocol_mqtt() {
456        let protocol = Protocol::Mqtt;
457        assert!(matches!(protocol, Protocol::Mqtt));
458    }
459
460    #[test]
461    fn test_protocol_smtp() {
462        let protocol = Protocol::Smtp;
463        assert!(matches!(protocol, Protocol::Smtp));
464    }
465
466    #[test]
467    fn test_protocol_kafka() {
468        let protocol = Protocol::Kafka;
469        assert!(matches!(protocol, Protocol::Kafka));
470    }
471
472    #[test]
473    fn test_protocol_amqp() {
474        let protocol = Protocol::Amqp;
475        assert!(matches!(protocol, Protocol::Amqp));
476    }
477
478    #[test]
479    fn test_protocol_ftp() {
480        let protocol = Protocol::Ftp;
481        assert!(matches!(protocol, Protocol::Ftp));
482    }
483
484    // ==================== Graph Structure Tests ====================
485
486    #[test]
487    fn test_graph_empty_clusters() {
488        let builder = GraphBuilder::new();
489        let graph = builder.build();
490        assert!(graph.clusters.is_empty());
491    }
492
493    #[test]
494    fn test_graph_empty_edges() {
495        let builder = GraphBuilder::new();
496        let graph = builder.build();
497        assert!(graph.edges.is_empty());
498    }
499
500    // ==================== Edge Cases ====================
501
502    #[test]
503    fn test_graph_builder_empty_metadata() {
504        let mut builder = GraphBuilder::new();
505        let metadata = std::collections::HashMap::new();
506
507        builder.add_endpoint(
508            "minimal".to_string(),
509            "Minimal Endpoint".to_string(),
510            Protocol::Http,
511            metadata,
512        );
513
514        let graph = builder.build();
515        assert_eq!(graph.nodes.len(), 1);
516    }
517
518    #[test]
519    fn test_graph_builder_unicode_names() {
520        let mut builder = GraphBuilder::new();
521        let metadata = std::collections::HashMap::new();
522
523        builder.add_endpoint(
524            "unicode-日本語".to_string(),
525            "ユニコード".to_string(),
526            Protocol::Http,
527            metadata,
528        );
529
530        let graph = builder.build();
531        assert_eq!(graph.nodes.len(), 1);
532    }
533
534    #[test]
535    fn test_graph_builder_special_characters() {
536        let mut builder = GraphBuilder::new();
537        let metadata = std::collections::HashMap::new();
538
539        builder.add_endpoint(
540            "special-!@#$%".to_string(),
541            "Special <>&'\"".to_string(),
542            Protocol::Http,
543            metadata,
544        );
545
546        let graph = builder.build();
547        assert_eq!(graph.nodes.len(), 1);
548    }
549
550    #[test]
551    fn test_graph_builder_long_names() {
552        let mut builder = GraphBuilder::new();
553        let metadata = std::collections::HashMap::new();
554        let long_id = "a".repeat(1000);
555        let long_name = "b".repeat(1000);
556
557        builder.add_endpoint(long_id, long_name, Protocol::Http, metadata);
558
559        let graph = builder.build();
560        assert_eq!(graph.nodes.len(), 1);
561    }
562
563    #[test]
564    fn test_graph_builder_complex_metadata() {
565        let mut builder = GraphBuilder::new();
566        let mut metadata = std::collections::HashMap::new();
567        metadata.insert("nested".to_string(), serde_json::json!({"key": {"inner": "value"}}));
568        metadata.insert("array".to_string(), serde_json::json!([1, 2, 3]));
569        metadata.insert("null".to_string(), Value::Null);
570
571        builder.add_endpoint(
572            "complex".to_string(),
573            "Complex Metadata".to_string(),
574            Protocol::Http,
575            metadata,
576        );
577
578        let graph = builder.build();
579        assert_eq!(graph.nodes.len(), 1);
580    }
581
582    #[test]
583    fn test_graph_builder_duplicate_ids() {
584        let mut builder = GraphBuilder::new();
585        let metadata = std::collections::HashMap::new();
586
587        // Add same ID twice - behavior depends on implementation
588        builder.add_endpoint(
589            "same-id".to_string(),
590            "First".to_string(),
591            Protocol::Http,
592            metadata.clone(),
593        );
594        builder.add_endpoint("same-id".to_string(), "Second".to_string(), Protocol::Grpc, metadata);
595
596        let graph = builder.build();
597        // Either 1 (replaced) or 2 (both added) depending on implementation
598        assert!(graph.nodes.len() >= 1);
599    }
600}