1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct MockConfig {
23 pub id: String,
25 pub name: String,
27 pub method: String,
29 pub path: String,
31 pub response: MockResponse,
33 pub enabled: bool,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub latency_ms: Option<u64>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub status_code: Option<u16>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct MockResponse {
46 pub body: serde_json::Value,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub headers: Option<std::collections::HashMap<String, String>>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ServerStats {
56 pub uptime_seconds: u64,
58 pub total_requests: u64,
60 pub active_mocks: usize,
62 pub enabled_mocks: usize,
64 pub registered_routes: usize,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ServerConfig {
71 pub version: String,
73 pub port: u16,
75 pub has_openapi_spec: bool,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub spec_path: Option<String>,
80}
81
82#[derive(Clone)]
84pub struct ManagementState {
85 pub mocks: Arc<RwLock<Vec<MockConfig>>>,
87 pub spec: Option<Arc<OpenApiSpec>>,
89 pub spec_path: Option<String>,
91 pub port: u16,
93 pub start_time: std::time::Instant,
95 pub request_counter: Arc<RwLock<u64>>,
97 #[cfg(feature = "smtp")]
99 pub smtp_registry: Option<Arc<mockforge_smtp::SmtpSpecRegistry>>,
100 #[cfg(feature = "mqtt")]
102 pub mqtt_broker: Option<Arc<mockforge_mqtt::MqttBroker>>,
103}
104
105impl ManagementState {
106 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 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 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
145async 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
155async 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
169async 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 if mock.id.is_empty() {
178 mock.id = uuid::Uuid::new_v4().to_string();
179 }
180
181 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
191async 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
206async 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
220async 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(), })
232}
233
234async 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
244async 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#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "lowercase")]
256pub enum ExportFormat {
257 Json,
259 Yaml,
261}
262
263async 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
288async 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")]
300async 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#[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#[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#[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#[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#[cfg(feature = "mqtt")]
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct MqttBrokerStats {
454 pub connected_clients: usize,
456 pub active_topics: usize,
458 pub retained_messages: usize,
460 pub total_subscriptions: usize,
462}
463
464#[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
531pub 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
569pub 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 let management = management_router(state);
578
579 let ui_builder_state = UIBuilderState::new(server_config);
581 let ui_builder = create_ui_builder_router(ui_builder_state);
582
583 management.nest("/ui-builder", ui_builder)
585}
586
587pub fn management_router_with_spec_import(state: ManagementState) -> Router {
589 use crate::spec_import::{spec_import_router, SpecImportState};
590
591 let management = management_router(state);
593
594 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 {
624 let mut mocks = state.mocks.write().await;
625 mocks.push(mock.clone());
626 }
627
628 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 {
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}