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