mockforge_http/
management.rs

1/// Management API for MockForge
2///
3/// Provides REST endpoints for controlling mocks, server configuration,
4/// and integration with developer tools (VS Code extension, CI/CD, etc.)
5use axum::{
6    extract::{Path, State},
7    http::StatusCode,
8    response::{IntoResponse, Json},
9    routing::{delete, get, post, put},
10    Router,
11};
12use mockforge_core::openapi::OpenApiSpec;
13#[cfg(feature = "smtp")]
14use mockforge_smtp::EmailSearchFilters;
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use tokio::sync::RwLock;
18use tracing::*;
19
20/// Mock configuration representation
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct MockConfig {
23    /// Unique identifier for the mock
24    pub id: String,
25    /// Human-readable name for the mock
26    pub name: String,
27    /// HTTP method (GET, POST, etc.)
28    pub method: String,
29    /// API path pattern to match
30    pub path: String,
31    /// Response configuration
32    pub response: MockResponse,
33    /// Whether this mock is currently enabled
34    pub enabled: bool,
35    /// Optional latency to inject in milliseconds
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub latency_ms: Option<u64>,
38    /// Optional HTTP status code override
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub status_code: Option<u16>,
41}
42
43/// Mock response configuration
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct MockResponse {
46    /// Response body as JSON
47    pub body: serde_json::Value,
48    /// Optional custom response headers
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub headers: Option<std::collections::HashMap<String, String>>,
51}
52
53/// Server statistics
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ServerStats {
56    /// Server uptime in seconds
57    pub uptime_seconds: u64,
58    /// Total number of requests processed
59    pub total_requests: u64,
60    /// Number of active mock configurations
61    pub active_mocks: usize,
62    /// Number of currently enabled mocks
63    pub enabled_mocks: usize,
64    /// Number of registered API routes
65    pub registered_routes: usize,
66}
67
68/// Server configuration info
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ServerConfig {
71    /// MockForge version string
72    pub version: String,
73    /// Server port number
74    pub port: u16,
75    /// Whether an OpenAPI spec is loaded
76    pub has_openapi_spec: bool,
77    /// Optional path to the OpenAPI spec file
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub spec_path: Option<String>,
80}
81
82/// Shared state for the management API
83#[derive(Clone)]
84pub struct ManagementState {
85    /// Collection of mock configurations
86    pub mocks: Arc<RwLock<Vec<MockConfig>>>,
87    /// Optional OpenAPI specification
88    pub spec: Option<Arc<OpenApiSpec>>,
89    /// Optional path to the OpenAPI spec file
90    pub spec_path: Option<String>,
91    /// Server port number
92    pub port: u16,
93    /// Server start time for uptime calculation
94    pub start_time: std::time::Instant,
95    /// Counter for total requests processed
96    pub request_counter: Arc<RwLock<u64>>,
97    /// Optional SMTP registry for email mocking
98    #[cfg(feature = "smtp")]
99    pub smtp_registry: Option<Arc<mockforge_smtp::SmtpSpecRegistry>>,
100    /// Optional MQTT broker for message mocking
101    #[cfg(feature = "mqtt")]
102    pub mqtt_broker: Option<Arc<mockforge_mqtt::MqttBroker>>,
103}
104
105impl ManagementState {
106    /// Create a new management state
107    ///
108    /// # Arguments
109    /// * `spec` - Optional OpenAPI specification
110    /// * `spec_path` - Optional path to the OpenAPI spec file
111    /// * `port` - Server port number
112    pub fn new(spec: Option<Arc<OpenApiSpec>>, spec_path: Option<String>, port: u16) -> Self {
113        Self {
114            mocks: Arc::new(RwLock::new(Vec::new())),
115            spec,
116            spec_path,
117            port,
118            start_time: std::time::Instant::now(),
119            request_counter: Arc::new(RwLock::new(0)),
120            #[cfg(feature = "smtp")]
121            smtp_registry: None,
122            #[cfg(feature = "mqtt")]
123            mqtt_broker: None,
124        }
125    }
126
127    #[cfg(feature = "smtp")]
128    /// Add SMTP registry to management state
129    pub fn with_smtp_registry(
130        mut self,
131        smtp_registry: Arc<mockforge_smtp::SmtpSpecRegistry>,
132    ) -> Self {
133        self.smtp_registry = Some(smtp_registry);
134        self
135    }
136
137    #[cfg(feature = "mqtt")]
138    /// Add MQTT broker to management state
139    pub fn with_mqtt_broker(mut self, mqtt_broker: Arc<mockforge_mqtt::MqttBroker>) -> Self {
140        self.mqtt_broker = Some(mqtt_broker);
141        self
142    }
143}
144
145/// List all mocks
146async fn list_mocks(State(state): State<ManagementState>) -> Json<serde_json::Value> {
147    let mocks = state.mocks.read().await;
148    Json(serde_json::json!({
149        "mocks": *mocks,
150        "total": mocks.len(),
151        "enabled": mocks.iter().filter(|m| m.enabled).count()
152    }))
153}
154
155/// Get a specific mock by ID
156async fn get_mock(
157    State(state): State<ManagementState>,
158    Path(id): Path<String>,
159) -> Result<Json<MockConfig>, StatusCode> {
160    let mocks = state.mocks.read().await;
161    mocks
162        .iter()
163        .find(|m| m.id == id)
164        .cloned()
165        .map(Json)
166        .ok_or(StatusCode::NOT_FOUND)
167}
168
169/// Create a new mock
170async fn create_mock(
171    State(state): State<ManagementState>,
172    Json(mut mock): Json<MockConfig>,
173) -> Result<Json<MockConfig>, StatusCode> {
174    let mut mocks = state.mocks.write().await;
175
176    // Generate ID if not provided
177    if mock.id.is_empty() {
178        mock.id = uuid::Uuid::new_v4().to_string();
179    }
180
181    // Check for duplicate ID
182    if mocks.iter().any(|m| m.id == mock.id) {
183        return Err(StatusCode::CONFLICT);
184    }
185
186    info!("Creating mock: {} {} {}", mock.method, mock.path, mock.id);
187    mocks.push(mock.clone());
188    Ok(Json(mock))
189}
190
191/// Update an existing mock
192async fn update_mock(
193    State(state): State<ManagementState>,
194    Path(id): Path<String>,
195    Json(updated_mock): Json<MockConfig>,
196) -> Result<Json<MockConfig>, StatusCode> {
197    let mut mocks = state.mocks.write().await;
198
199    let position = mocks.iter().position(|m| m.id == id).ok_or(StatusCode::NOT_FOUND)?;
200
201    info!("Updating mock: {}", id);
202    mocks[position] = updated_mock.clone();
203    Ok(Json(updated_mock))
204}
205
206/// Delete a mock
207async fn delete_mock(
208    State(state): State<ManagementState>,
209    Path(id): Path<String>,
210) -> Result<StatusCode, StatusCode> {
211    let mut mocks = state.mocks.write().await;
212
213    let position = mocks.iter().position(|m| m.id == id).ok_or(StatusCode::NOT_FOUND)?;
214
215    info!("Deleting mock: {}", id);
216    mocks.remove(position);
217    Ok(StatusCode::NO_CONTENT)
218}
219
220/// Get server statistics
221async fn get_stats(State(state): State<ManagementState>) -> Json<ServerStats> {
222    let mocks = state.mocks.read().await;
223    let request_count = *state.request_counter.read().await;
224
225    Json(ServerStats {
226        uptime_seconds: state.start_time.elapsed().as_secs(),
227        total_requests: request_count,
228        active_mocks: mocks.len(),
229        enabled_mocks: mocks.iter().filter(|m| m.enabled).count(),
230        registered_routes: mocks.len(), // This could be enhanced with actual route registry info
231    })
232}
233
234/// Get server configuration
235async fn get_config(State(state): State<ManagementState>) -> Json<ServerConfig> {
236    Json(ServerConfig {
237        version: env!("CARGO_PKG_VERSION").to_string(),
238        port: state.port,
239        has_openapi_spec: state.spec.is_some(),
240        spec_path: state.spec_path.clone(),
241    })
242}
243
244/// Health check endpoint
245async fn health_check() -> Json<serde_json::Value> {
246    Json(serde_json::json!({
247        "status": "healthy",
248        "service": "mockforge-management",
249        "timestamp": chrono::Utc::now().to_rfc3339()
250    }))
251}
252
253/// Export format for mock configurations
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "lowercase")]
256pub enum ExportFormat {
257    /// JSON format
258    Json,
259    /// YAML format
260    Yaml,
261}
262
263/// Export mocks in specified format
264async fn export_mocks(
265    State(state): State<ManagementState>,
266    axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
267) -> Result<(StatusCode, String), StatusCode> {
268    let mocks = state.mocks.read().await;
269
270    let format = params
271        .get("format")
272        .map(|f| match f.as_str() {
273            "yaml" | "yml" => ExportFormat::Yaml,
274            _ => ExportFormat::Json,
275        })
276        .unwrap_or(ExportFormat::Json);
277
278    match format {
279        ExportFormat::Json => serde_json::to_string_pretty(&*mocks)
280            .map(|json| (StatusCode::OK, json))
281            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
282        ExportFormat::Yaml => serde_yaml::to_string(&*mocks)
283            .map(|yaml| (StatusCode::OK, yaml))
284            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
285    }
286}
287
288/// Import mocks from JSON/YAML
289async fn import_mocks(
290    State(state): State<ManagementState>,
291    Json(mocks): Json<Vec<MockConfig>>,
292) -> impl IntoResponse {
293    let mut current_mocks = state.mocks.write().await;
294    current_mocks.clear();
295    current_mocks.extend(mocks);
296    Json(serde_json::json!({ "status": "imported", "count": current_mocks.len() }))
297}
298
299#[cfg(feature = "smtp")]
300/// List SMTP emails in mailbox
301async fn list_smtp_emails(State(state): State<ManagementState>) -> impl IntoResponse {
302    if let Some(ref smtp_registry) = state.smtp_registry {
303        match smtp_registry.get_emails() {
304            Ok(emails) => (StatusCode::OK, Json(serde_json::json!(emails))),
305            Err(e) => (
306                StatusCode::INTERNAL_SERVER_ERROR,
307                Json(serde_json::json!({
308                    "error": "Failed to retrieve emails",
309                    "message": e.to_string()
310                })),
311            ),
312        }
313    } else {
314        (
315            StatusCode::NOT_IMPLEMENTED,
316            Json(serde_json::json!({
317                "error": "SMTP mailbox management not available",
318                "message": "SMTP server is not enabled or registry not available."
319            })),
320        )
321    }
322}
323
324/// Get specific SMTP email
325#[cfg(feature = "smtp")]
326async fn get_smtp_email(
327    State(state): State<ManagementState>,
328    Path(id): Path<String>,
329) -> impl IntoResponse {
330    if let Some(ref smtp_registry) = state.smtp_registry {
331        match smtp_registry.get_email_by_id(&id) {
332            Ok(Some(email)) => (StatusCode::OK, Json(serde_json::json!(email))),
333            Ok(None) => (
334                StatusCode::NOT_FOUND,
335                Json(serde_json::json!({
336                    "error": "Email not found",
337                    "id": id
338                })),
339            ),
340            Err(e) => (
341                StatusCode::INTERNAL_SERVER_ERROR,
342                Json(serde_json::json!({
343                    "error": "Failed to retrieve email",
344                    "message": e.to_string()
345                })),
346            ),
347        }
348    } else {
349        (
350            StatusCode::NOT_IMPLEMENTED,
351            Json(serde_json::json!({
352                "error": "SMTP mailbox management not available",
353                "message": "SMTP server is not enabled or registry not available."
354            })),
355        )
356    }
357}
358
359/// Clear SMTP mailbox
360#[cfg(feature = "smtp")]
361async fn clear_smtp_mailbox(State(state): State<ManagementState>) -> impl IntoResponse {
362    if let Some(ref smtp_registry) = state.smtp_registry {
363        match smtp_registry.clear_mailbox() {
364            Ok(()) => (
365                StatusCode::OK,
366                Json(serde_json::json!({
367                    "message": "Mailbox cleared successfully"
368                })),
369            ),
370            Err(e) => (
371                StatusCode::INTERNAL_SERVER_ERROR,
372                Json(serde_json::json!({
373                    "error": "Failed to clear mailbox",
374                    "message": e.to_string()
375                })),
376            ),
377        }
378    } else {
379        (
380            StatusCode::NOT_IMPLEMENTED,
381            Json(serde_json::json!({
382                "error": "SMTP mailbox management not available",
383                "message": "SMTP server is not enabled or registry not available."
384            })),
385        )
386    }
387}
388
389/// Export SMTP mailbox
390#[cfg(feature = "smtp")]
391async fn export_smtp_mailbox(
392    axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
393) -> impl IntoResponse {
394    let format = params.get("format").unwrap_or(&"json".to_string()).clone();
395    (
396        StatusCode::NOT_IMPLEMENTED,
397        Json(serde_json::json!({
398            "error": "SMTP mailbox management not available via HTTP API",
399            "message": "SMTP server runs separately from HTTP server. Use CLI commands to access mailbox.",
400            "requested_format": format
401        })),
402    )
403}
404
405/// Search SMTP emails
406#[cfg(feature = "smtp")]
407async fn search_smtp_emails(
408    State(state): State<ManagementState>,
409    axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
410) -> impl IntoResponse {
411    if let Some(ref smtp_registry) = state.smtp_registry {
412        let filters = EmailSearchFilters {
413            sender: params.get("sender").cloned(),
414            recipient: params.get("recipient").cloned(),
415            subject: params.get("subject").cloned(),
416            body: params.get("body").cloned(),
417            since: params
418                .get("since")
419                .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
420                .map(|dt| dt.with_timezone(&chrono::Utc)),
421            until: params
422                .get("until")
423                .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
424                .map(|dt| dt.with_timezone(&chrono::Utc)),
425            use_regex: params.get("regex").map(|s| s == "true").unwrap_or(false),
426            case_sensitive: params.get("case_sensitive").map(|s| s == "true").unwrap_or(false),
427        };
428
429        match smtp_registry.search_emails(filters) {
430            Ok(emails) => (StatusCode::OK, Json(serde_json::json!(emails))),
431            Err(e) => (
432                StatusCode::INTERNAL_SERVER_ERROR,
433                Json(serde_json::json!({
434                    "error": "Failed to search emails",
435                    "message": e.to_string()
436                })),
437            ),
438        }
439    } else {
440        (
441            StatusCode::NOT_IMPLEMENTED,
442            Json(serde_json::json!({
443                "error": "SMTP mailbox management not available",
444                "message": "SMTP server is not enabled or registry not available."
445            })),
446        )
447    }
448}
449
450/// MQTT broker statistics
451#[cfg(feature = "mqtt")]
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct MqttBrokerStats {
454    /// Number of connected MQTT clients
455    pub connected_clients: usize,
456    /// Number of active MQTT topics
457    pub active_topics: usize,
458    /// Number of retained messages
459    pub retained_messages: usize,
460    /// Total number of subscriptions
461    pub total_subscriptions: usize,
462}
463
464/// MQTT management handlers
465#[cfg(feature = "mqtt")]
466async fn get_mqtt_stats(State(state): State<ManagementState>) -> impl IntoResponse {
467    if let Some(broker) = &state.mqtt_broker {
468        let connected_clients = broker.get_connected_clients().await.len();
469        let active_topics = broker.get_active_topics().await.len();
470        let stats = broker.get_topic_stats().await;
471
472        let broker_stats = MqttBrokerStats {
473            connected_clients,
474            active_topics,
475            retained_messages: stats.retained_messages,
476            total_subscriptions: stats.total_subscriptions,
477        };
478
479        Json(broker_stats).into_response()
480    } else {
481        (StatusCode::SERVICE_UNAVAILABLE, "MQTT broker not available").into_response()
482    }
483}
484
485#[cfg(feature = "mqtt")]
486async fn get_mqtt_clients(State(state): State<ManagementState>) -> impl IntoResponse {
487    if let Some(broker) = &state.mqtt_broker {
488        let clients = broker.get_connected_clients().await;
489        Json(serde_json::json!({
490            "clients": clients
491        }))
492        .into_response()
493    } else {
494        (StatusCode::SERVICE_UNAVAILABLE, "MQTT broker not available").into_response()
495    }
496}
497
498#[cfg(feature = "mqtt")]
499async fn get_mqtt_topics(State(state): State<ManagementState>) -> impl IntoResponse {
500    if let Some(broker) = &state.mqtt_broker {
501        let topics = broker.get_active_topics().await;
502        Json(serde_json::json!({
503            "topics": topics
504        }))
505        .into_response()
506    } else {
507        (StatusCode::SERVICE_UNAVAILABLE, "MQTT broker not available").into_response()
508    }
509}
510
511#[cfg(feature = "mqtt")]
512async fn disconnect_mqtt_client(
513    State(state): State<ManagementState>,
514    Path(client_id): Path<String>,
515) -> impl IntoResponse {
516    if let Some(broker) = &state.mqtt_broker {
517        match broker.disconnect_client(&client_id).await {
518            Ok(_) => {
519                (StatusCode::OK, format!("Client '{}' disconnected", client_id)).into_response()
520            }
521            Err(e) => {
522                (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to disconnect client: {}", e))
523                    .into_response()
524            }
525        }
526    } else {
527        (StatusCode::SERVICE_UNAVAILABLE, "MQTT broker not available").into_response()
528    }
529}
530
531/// Build the management API router
532pub fn management_router(state: ManagementState) -> Router {
533    let router = Router::new()
534        .route("/health", get(health_check))
535        .route("/stats", get(get_stats))
536        .route("/config", get(get_config))
537        .route("/mocks", get(list_mocks))
538        .route("/mocks", post(create_mock))
539        .route("/mocks/{id}", get(get_mock))
540        .route("/mocks/{id}", put(update_mock))
541        .route("/mocks/{id}", delete(delete_mock))
542        .route("/export", get(export_mocks))
543        .route("/import", post(import_mocks));
544
545    #[cfg(feature = "smtp")]
546    let router = router
547        .route("/smtp/mailbox", get(list_smtp_emails))
548        .route("/smtp/mailbox", delete(clear_smtp_mailbox))
549        .route("/smtp/mailbox/{id}", get(get_smtp_email))
550        .route("/smtp/mailbox/export", get(export_smtp_mailbox))
551        .route("/smtp/mailbox/search", get(search_smtp_emails));
552
553    #[cfg(not(feature = "smtp"))]
554    let router = router;
555
556    #[cfg(feature = "mqtt")]
557    let router = router
558        .route("/mqtt/stats", get(get_mqtt_stats))
559        .route("/mqtt/clients", get(get_mqtt_clients))
560        .route("/mqtt/topics", get(get_mqtt_topics))
561        .route("/mqtt/clients/{client_id}", delete(disconnect_mqtt_client));
562
563    #[cfg(not(feature = "mqtt"))]
564    let router = router;
565
566    router.with_state(state)
567}
568
569/// Build the management API router with UI Builder support
570pub fn management_router_with_ui_builder(
571    state: ManagementState,
572    server_config: mockforge_core::config::ServerConfig,
573) -> Router {
574    use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
575
576    // Create the base management router
577    let management = management_router(state);
578
579    // Create UI Builder state and router
580    let ui_builder_state = UIBuilderState::new(server_config);
581    let ui_builder = create_ui_builder_router(ui_builder_state);
582
583    // Nest UI Builder under /ui-builder
584    management.nest("/ui-builder", ui_builder)
585}
586
587/// Build management router with spec import API
588pub fn management_router_with_spec_import(state: ManagementState) -> Router {
589    use crate::spec_import::{spec_import_router, SpecImportState};
590
591    // Create base management router
592    let management = management_router(state);
593
594    // Merge with spec import router
595    Router::new()
596        .merge(management)
597        .merge(spec_import_router(SpecImportState::new()))
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603
604    #[tokio::test]
605    async fn test_create_and_get_mock() {
606        let state = ManagementState::new(None, None, 3000);
607
608        let mock = MockConfig {
609            id: "test-1".to_string(),
610            name: "Test Mock".to_string(),
611            method: "GET".to_string(),
612            path: "/test".to_string(),
613            response: MockResponse {
614                body: serde_json::json!({"message": "test"}),
615                headers: None,
616            },
617            enabled: true,
618            latency_ms: None,
619            status_code: Some(200),
620        };
621
622        // Create mock
623        {
624            let mut mocks = state.mocks.write().await;
625            mocks.push(mock.clone());
626        }
627
628        // Get mock
629        let mocks = state.mocks.read().await;
630        let found = mocks.iter().find(|m| m.id == "test-1");
631        assert!(found.is_some());
632        assert_eq!(found.unwrap().name, "Test Mock");
633    }
634
635    #[tokio::test]
636    async fn test_server_stats() {
637        let state = ManagementState::new(None, None, 3000);
638
639        // Add some mocks
640        {
641            let mut mocks = state.mocks.write().await;
642            mocks.push(MockConfig {
643                id: "1".to_string(),
644                name: "Mock 1".to_string(),
645                method: "GET".to_string(),
646                path: "/test1".to_string(),
647                response: MockResponse {
648                    body: serde_json::json!({}),
649                    headers: None,
650                },
651                enabled: true,
652                latency_ms: None,
653                status_code: Some(200),
654            });
655            mocks.push(MockConfig {
656                id: "2".to_string(),
657                name: "Mock 2".to_string(),
658                method: "POST".to_string(),
659                path: "/test2".to_string(),
660                response: MockResponse {
661                    body: serde_json::json!({}),
662                    headers: None,
663                },
664                enabled: false,
665                latency_ms: None,
666                status_code: Some(201),
667            });
668        }
669
670        let mocks = state.mocks.read().await;
671        assert_eq!(mocks.len(), 2);
672        assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
673    }
674}