Skip to main content

mockforge_ui/handlers/
admin.rs

1//! Admin dashboard and server management handlers
2//!
3//! This module handles admin dashboard operations, server management,
4//! metrics, logs, and configuration.
5
6use axum::{
7    extract::{Query, State},
8    response::Json,
9};
10use chrono::Utc;
11use serde_json::{json, Value};
12use std::collections::HashMap;
13
14use crate::handlers::AdminState;
15use crate::models::*;
16
17/// Request metrics for tracking
18#[derive(Debug, Clone, Default)]
19pub struct RequestMetrics {
20    /// Total requests served
21    pub total_requests: u64,
22    /// Active requests currently being processed
23    pub active_requests: u64,
24    /// Average response time in milliseconds
25    pub average_response_time: f64,
26    /// Request rate per second
27    pub requests_per_second: f64,
28    /// Total errors encountered
29    pub total_errors: u64,
30}
31
32/// Get server information
33pub async fn get_server_info(State(state): State<AdminState>) -> Json<Value> {
34    Json(json!({
35        "http_server": state.http_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
36        "ws_server": state.ws_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
37        "grpc_server": state.grpc_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
38        "graphql_server": state.graphql_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
39        "api_enabled": state.api_enabled,
40    }))
41}
42
43/// Get health check status
44pub async fn get_health() -> Json<HealthCheck> {
45    Json(HealthCheck {
46        status: "healthy".to_string(),
47        services: HashMap::new(),
48        last_check: Utc::now(),
49        issues: Vec::new(),
50    })
51}
52
53/// Get logs
54pub async fn get_logs(
55    State(_state): State<AdminState>,
56    Query(params): Query<HashMap<String, String>>,
57) -> Json<ApiResponse<Vec<LogEntry>>> {
58    // Parse query parameters for filtering
59    let limit = params.get("limit").and_then(|s| s.parse::<usize>().ok()).unwrap_or(100);
60
61    let method_filter = params.get("method").map(|s| s.to_string());
62    let path_filter = params.get("path").map(|s| s.to_string());
63    let status_filter = params.get("status").and_then(|s| s.parse::<u16>().ok());
64
65    // Get recent logs from the centralized logger
66    let request_logs = if let Some(global_logger) = mockforge_core::get_global_logger() {
67        global_logger.get_recent_logs(Some(limit * 2)).await
68    } else {
69        Vec::new()
70    };
71
72    // Convert RequestLogEntry to LogEntry and apply filters
73    let mut log_entries: Vec<LogEntry> = request_logs
74        .into_iter()
75        .filter(|log| {
76            // Only include HTTP logs for now (matching the UI interface)
77            log.server_type == "HTTP"
78        })
79        .filter(|log| {
80            // Apply method filter
81            method_filter.as_ref().is_none_or(|filter| log.method == *filter)
82        })
83        .filter(|log| {
84            // Apply path filter (simple substring match)
85            path_filter.as_ref().is_none_or(|filter| log.path.contains(filter))
86        })
87        .filter(|log| {
88            // Apply status filter
89            status_filter.is_none_or(|filter| log.status_code == filter)
90        })
91        .map(|log| LogEntry {
92            timestamp: log.timestamp,
93            status: log.status_code,
94            method: log.method,
95            url: log.path,
96            response_time: log.response_time_ms,
97            size: log.response_size_bytes,
98        })
99        .take(limit)
100        .collect();
101
102    // Sort by timestamp descending (most recent first)
103    log_entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
104
105    Json(ApiResponse::success(log_entries))
106}
107
108/// Get metrics data
109pub async fn get_metrics(State(state): State<AdminState>) -> Json<ApiResponse<SimpleMetricsData>> {
110    let metrics = state.metrics.read().await;
111
112    let total_errors: u64 = metrics.errors_by_endpoint.values().sum();
113    let error_rate = if metrics.total_requests > 0 {
114        total_errors as f64 / metrics.total_requests as f64
115    } else {
116        0.0
117    };
118
119    let average_response_time = if metrics.response_times.is_empty() {
120        0.0
121    } else {
122        metrics.response_times.iter().sum::<u64>() as f64 / metrics.response_times.len() as f64
123    };
124
125    Json(ApiResponse::success(SimpleMetricsData {
126        total_requests: metrics.total_requests,
127        active_requests: metrics.active_connections,
128        average_response_time,
129        error_rate,
130    }))
131}
132
133/// Update latency configuration
134pub async fn update_latency(
135    State(state): State<AdminState>,
136    Json(config): Json<Value>,
137) -> Json<ApiResponse<String>> {
138    // Extract latency configuration from the JSON
139    let base_ms = config.get("base_ms").and_then(|v| v.as_u64()).unwrap_or(50);
140    let jitter_ms = config.get("jitter_ms").and_then(|v| v.as_u64()).unwrap_or(20);
141    let tag_overrides = config
142        .get("tag_overrides")
143        .and_then(|v| v.as_object())
144        .map(|obj| obj.iter().filter_map(|(k, v)| v.as_u64().map(|val| (k.clone(), val))).collect())
145        .unwrap_or_default();
146
147    // Update the configuration
148    state.update_latency_config(base_ms, jitter_ms, tag_overrides).await;
149
150    tracing::info!("Updated latency profile: base_ms={}, jitter_ms={}", base_ms, jitter_ms);
151    Json(ApiResponse::success("Latency configuration updated".to_string()))
152}
153
154/// Update fault injection configuration
155pub async fn update_faults(
156    State(state): State<AdminState>,
157    Json(config): Json<Value>,
158) -> Json<ApiResponse<String>> {
159    // Extract fault configuration from the JSON
160    let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
161    let failure_rate = config.get("failure_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
162    let status_codes = config
163        .get("status_codes")
164        .and_then(|v| v.as_array())
165        .map(|arr| arr.iter().filter_map(|v| v.as_u64().map(|n| n as u16)).collect())
166        .unwrap_or_default();
167
168    // Update the configuration
169    state.update_fault_config(enabled, failure_rate, status_codes).await;
170
171    tracing::info!("Updated fault config: enabled={}, failure_rate={}", enabled, failure_rate);
172    Json(ApiResponse::success("Fault configuration updated".to_string()))
173}
174
175/// Update proxy configuration
176pub async fn update_proxy(
177    State(state): State<AdminState>,
178    Json(config): Json<Value>,
179) -> Json<ApiResponse<String>> {
180    // Extract proxy configuration from the JSON
181    let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
182    let upstream_url = config.get("upstream_url").and_then(|v| v.as_str()).map(|s| s.to_string());
183    let timeout_seconds = config.get("timeout_seconds").and_then(|v| v.as_u64()).unwrap_or(30);
184
185    // Update the configuration
186    state.update_proxy_config(enabled, upstream_url.clone(), timeout_seconds).await;
187
188    tracing::info!(
189        "Updated proxy config: enabled={}, upstream_url={:?}, timeout_seconds={}",
190        enabled,
191        upstream_url,
192        timeout_seconds
193    );
194    Json(ApiResponse::success("Proxy configuration updated".to_string()))
195}
196
197/// Clear logs
198pub async fn clear_logs(State(_state): State<AdminState>) -> Json<ApiResponse<String>> {
199    if let Some(global_logger) = mockforge_core::get_global_logger() {
200        global_logger.clear_logs().await;
201    }
202    tracing::info!("Request logs cleared via admin UI");
203    Json(ApiResponse::success("Logs cleared".to_string()))
204}
205
206/// Restart servers
207pub async fn restart_servers(State(state): State<AdminState>) -> Json<ApiResponse<String>> {
208    // Check if restart is already in progress
209    let current_status = state.get_restart_status().await;
210    if current_status.in_progress {
211        return Json(ApiResponse::error("Server restart already in progress".to_string()));
212    }
213
214    // Initiate restart status
215    if let Err(e) = state
216        .initiate_restart("Manual restart requested via admin UI".to_string())
217        .await
218    {
219        return Json(ApiResponse::error(format!("Failed to initiate restart: {}", e)));
220    }
221
222    // Spawn restart task to avoid blocking the response
223    let state_clone = state.clone();
224    tokio::spawn(async move {
225        if let Err(e) = super::perform_server_restart(&state_clone).await {
226            tracing::error!("Server restart failed: {}", e);
227            state_clone.complete_restart(false).await;
228        } else {
229            tracing::info!("Server restart completed successfully");
230            state_clone.complete_restart(true).await;
231        }
232    });
233
234    tracing::info!("Server restart initiated via admin UI");
235    Json(ApiResponse::success(
236        "Server restart initiated. Please wait for completion.".to_string(),
237    ))
238}
239
240/// Get restart status
241pub async fn get_restart_status(
242    State(state): State<AdminState>,
243) -> Json<ApiResponse<super::RestartStatus>> {
244    let status = state.get_restart_status().await;
245    Json(ApiResponse::success(status))
246}
247
248/// Get configuration
249pub async fn get_config(State(state): State<AdminState>) -> Json<ApiResponse<Value>> {
250    let config = state.get_config().await;
251    Json(ApiResponse::success(serde_json::to_value(config).unwrap_or_else(|_| json!({}))))
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    fn create_test_state() -> AdminState {
259        AdminState::new(
260            None, None, None, None, false, 8080, None, None, None, None, None, None, None, None,
261        )
262    }
263
264    // ==================== RequestMetrics Tests ====================
265
266    #[test]
267    fn test_request_metrics_default() {
268        let metrics = RequestMetrics::default();
269        assert_eq!(metrics.total_requests, 0);
270        assert_eq!(metrics.active_requests, 0);
271        assert_eq!(metrics.average_response_time, 0.0);
272        assert_eq!(metrics.requests_per_second, 0.0);
273        assert_eq!(metrics.total_errors, 0);
274    }
275
276    #[test]
277    fn test_request_metrics_creation() {
278        let metrics = RequestMetrics {
279            total_requests: 1000,
280            active_requests: 10,
281            average_response_time: 45.5,
282            requests_per_second: 25.0,
283            total_errors: 5,
284        };
285
286        assert_eq!(metrics.total_requests, 1000);
287        assert_eq!(metrics.active_requests, 10);
288        assert!((metrics.average_response_time - 45.5).abs() < 0.001);
289        assert!((metrics.requests_per_second - 25.0).abs() < 0.001);
290        assert_eq!(metrics.total_errors, 5);
291    }
292
293    #[test]
294    fn test_request_metrics_clone() {
295        let metrics = RequestMetrics {
296            total_requests: 500,
297            active_requests: 5,
298            average_response_time: 30.0,
299            requests_per_second: 10.0,
300            total_errors: 2,
301        };
302
303        let cloned = metrics.clone();
304        assert_eq!(cloned.total_requests, 500);
305        assert_eq!(cloned.active_requests, 5);
306    }
307
308    #[test]
309    fn test_request_metrics_debug() {
310        let metrics = RequestMetrics::default();
311        let debug_str = format!("{:?}", metrics);
312        assert!(debug_str.contains("RequestMetrics"));
313        assert!(debug_str.contains("total_requests"));
314    }
315
316    // ==================== Handler Tests ====================
317
318    #[tokio::test]
319    async fn test_get_restart_status() {
320        let state = create_test_state();
321        let response = get_restart_status(State(state)).await;
322
323        assert!(response.0.success);
324    }
325
326    #[tokio::test]
327    async fn test_get_config() {
328        let state = create_test_state();
329        let response = get_config(State(state)).await;
330
331        assert!(response.0.success);
332    }
333
334    #[tokio::test]
335    async fn test_get_health() {
336        let response = get_health().await;
337
338        assert_eq!(response.0.status, "healthy");
339        assert!(response.0.issues.is_empty());
340    }
341
342    #[tokio::test]
343    async fn test_get_server_info() {
344        let state = create_test_state();
345        let response = get_server_info(State(state)).await;
346
347        assert!(response.0.is_object());
348        let obj = response.0.as_object().unwrap();
349        assert!(obj.contains_key("http_server"));
350        assert!(obj.contains_key("ws_server"));
351        assert!(obj.contains_key("grpc_server"));
352        assert!(obj.contains_key("graphql_server"));
353        assert!(obj.contains_key("api_enabled"));
354    }
355
356    #[tokio::test]
357    async fn test_get_server_info_disabled() {
358        let state = create_test_state();
359        let response = get_server_info(State(state)).await;
360
361        // With None addresses, should return "disabled"
362        let obj = response.0.as_object().unwrap();
363        assert_eq!(obj.get("http_server").and_then(|v| v.as_str()), Some("disabled"));
364        assert_eq!(obj.get("ws_server").and_then(|v| v.as_str()), Some("disabled"));
365    }
366
367    #[tokio::test]
368    async fn test_get_metrics() {
369        let state = create_test_state();
370        let response = get_metrics(State(state)).await;
371
372        assert!(response.0.success);
373    }
374
375    #[tokio::test]
376    async fn test_get_logs_empty() {
377        let state = create_test_state();
378        let params = HashMap::new();
379        let response = get_logs(State(state), Query(params)).await;
380
381        assert!(response.0.success);
382    }
383
384    #[tokio::test]
385    async fn test_get_logs_with_limit() {
386        let state = create_test_state();
387        let mut params = HashMap::new();
388        params.insert("limit".to_string(), "10".to_string());
389
390        let response = get_logs(State(state), Query(params)).await;
391
392        assert!(response.0.success);
393    }
394
395    #[tokio::test]
396    async fn test_get_logs_with_method_filter() {
397        let state = create_test_state();
398        let mut params = HashMap::new();
399        params.insert("method".to_string(), "GET".to_string());
400
401        let response = get_logs(State(state), Query(params)).await;
402
403        assert!(response.0.success);
404    }
405
406    #[tokio::test]
407    async fn test_get_logs_with_path_filter() {
408        let state = create_test_state();
409        let mut params = HashMap::new();
410        params.insert("path".to_string(), "/api".to_string());
411
412        let response = get_logs(State(state), Query(params)).await;
413
414        assert!(response.0.success);
415    }
416
417    #[tokio::test]
418    async fn test_get_logs_with_status_filter() {
419        let state = create_test_state();
420        let mut params = HashMap::new();
421        params.insert("status".to_string(), "200".to_string());
422
423        let response = get_logs(State(state), Query(params)).await;
424
425        assert!(response.0.success);
426    }
427
428    #[tokio::test]
429    async fn test_clear_logs() {
430        let state = create_test_state();
431        let response = clear_logs(State(state)).await;
432
433        assert!(response.0.success);
434        assert!(response.0.data.is_some());
435    }
436
437    #[tokio::test]
438    async fn test_update_latency() {
439        let state = create_test_state();
440        let config = json!({
441            "base_ms": 100,
442            "jitter_ms": 20
443        });
444
445        let response = update_latency(State(state), Json(config)).await;
446
447        assert!(response.0.success);
448    }
449
450    #[tokio::test]
451    async fn test_update_latency_with_overrides() {
452        let state = create_test_state();
453        let config = json!({
454            "base_ms": 50,
455            "jitter_ms": 10,
456            "tag_overrides": {
457                "slow": 500,
458                "fast": 10
459            }
460        });
461
462        let response = update_latency(State(state), Json(config)).await;
463
464        assert!(response.0.success);
465    }
466
467    #[tokio::test]
468    async fn test_update_faults() {
469        let state = create_test_state();
470        let config = json!({
471            "enabled": true,
472            "failure_rate": 0.1,
473            "status_codes": [500, 503]
474        });
475
476        let response = update_faults(State(state), Json(config)).await;
477
478        assert!(response.0.success);
479    }
480
481    #[tokio::test]
482    async fn test_update_faults_disabled() {
483        let state = create_test_state();
484        let config = json!({
485            "enabled": false
486        });
487
488        let response = update_faults(State(state), Json(config)).await;
489
490        assert!(response.0.success);
491    }
492
493    #[tokio::test]
494    async fn test_update_proxy() {
495        let state = create_test_state();
496        let config = json!({
497            "enabled": true,
498            "upstream_url": "http://localhost:8000",
499            "timeout_seconds": 60
500        });
501
502        let response = update_proxy(State(state), Json(config)).await;
503
504        assert!(response.0.success);
505    }
506
507    #[tokio::test]
508    async fn test_update_proxy_disabled() {
509        let state = create_test_state();
510        let config = json!({
511            "enabled": false
512        });
513
514        let response = update_proxy(State(state), Json(config)).await;
515
516        assert!(response.0.success);
517    }
518
519    #[tokio::test]
520    async fn test_restart_servers() {
521        let state = create_test_state();
522        let response = restart_servers(State(state)).await;
523
524        // Should succeed to initiate (even if restart won't actually work without real servers)
525        assert!(response.0.success || response.0.error.is_some());
526    }
527}