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;
13use mockforge_smtp::EmailSearchFilters;
14use serde::{Deserialize, Serialize};
15use std::sync::Arc;
16use tokio::sync::RwLock;
17use tracing::*;
18
19#[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#[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#[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#[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#[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
103async 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
113async 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
127async 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 if mock.id.is_empty() {
136 mock.id = uuid::Uuid::new_v4().to_string();
137 }
138
139 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
149async 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
164async 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
178async 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(), })
190}
191
192async 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
202async 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#[derive(Debug, Clone, Serialize, Deserialize)]
213#[serde(rename_all = "lowercase")]
214pub enum ExportFormat {
215 Json,
216 Yaml,
217}
218
219async 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
244async 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
255async 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
279async 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
313async 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
342async 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
357async 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#[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
410async 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
473pub 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 {
524 let mut mocks = state.mocks.write().await;
525 mocks.push(mock.clone());
526 }
527
528 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 {
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}