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