1use 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#[derive(Debug, Clone, Default)]
19pub struct RequestMetrics {
20 pub total_requests: u64,
22 pub active_requests: u64,
24 pub average_response_time: f64,
26 pub requests_per_second: f64,
28 pub total_errors: u64,
30}
31
32pub 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
43pub 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
53pub async fn get_logs(
55 State(_state): State<AdminState>,
56 Query(params): Query<HashMap<String, String>>,
57) -> Json<ApiResponse<Vec<LogEntry>>> {
58 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 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 let mut log_entries: Vec<LogEntry> = request_logs
74 .into_iter()
75 .filter(|log| {
76 log.server_type == "HTTP"
78 })
79 .filter(|log| {
80 method_filter.as_ref().is_none_or(|filter| log.method == *filter)
82 })
83 .filter(|log| {
84 path_filter.as_ref().is_none_or(|filter| log.path.contains(filter))
86 })
87 .filter(|log| {
88 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 log_entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
104
105 Json(ApiResponse::success(log_entries))
106}
107
108pub async fn get_metrics(State(state): State<AdminState>) -> Json<ApiResponse<SimpleMetricsData>> {
110 let metrics = state.metrics.read().await;
111 let error_rate = 0.0; Json(ApiResponse::success(SimpleMetricsData {
114 total_requests: metrics.total_requests,
115 active_requests: metrics.active_connections, average_response_time: 0.0, error_rate,
118 }))
119}
120
121pub async fn update_latency(
123 State(state): State<super::AdminState>,
124 Json(config): Json<Value>,
125) -> Json<ApiResponse<String>> {
126 let base_ms = config.get("base_ms").and_then(|v| v.as_u64()).unwrap_or(50);
128 let jitter_ms = config.get("jitter_ms").and_then(|v| v.as_u64()).unwrap_or(20);
129 let tag_overrides = config
130 .get("tag_overrides")
131 .and_then(|v| v.as_object())
132 .map(|obj| obj.iter().filter_map(|(k, v)| v.as_u64().map(|val| (k.clone(), val))).collect())
133 .unwrap_or_default();
134
135 state.update_latency_config(base_ms, jitter_ms, tag_overrides).await;
137
138 tracing::info!("Updated latency profile: base_ms={}, jitter_ms={}", base_ms, jitter_ms);
139 Json(ApiResponse::success("Latency configuration updated".to_string()))
140}
141
142pub async fn update_faults(
144 State(state): State<super::AdminState>,
145 Json(config): Json<Value>,
146) -> Json<ApiResponse<String>> {
147 let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
149 let failure_rate = config.get("failure_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
150 let status_codes = config
151 .get("status_codes")
152 .and_then(|v| v.as_array())
153 .map(|arr| arr.iter().filter_map(|v| v.as_u64().map(|n| n as u16)).collect())
154 .unwrap_or_default();
155
156 state.update_fault_config(enabled, failure_rate, status_codes).await;
158
159 tracing::info!("Updated fault config: enabled={}, failure_rate={}", enabled, failure_rate);
160 Json(ApiResponse::success("Fault configuration updated".to_string()))
161}
162
163pub async fn update_proxy(
165 State(state): State<super::AdminState>,
166 Json(config): Json<Value>,
167) -> Json<ApiResponse<String>> {
168 let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
170 let upstream_url = config.get("upstream_url").and_then(|v| v.as_str()).map(|s| s.to_string());
171 let timeout_seconds = config.get("timeout_seconds").and_then(|v| v.as_u64()).unwrap_or(30);
172
173 state.update_proxy_config(enabled, upstream_url.clone(), timeout_seconds).await;
175
176 tracing::info!(
177 "Updated proxy config: enabled={}, upstream_url={:?}, timeout_seconds={}",
178 enabled,
179 upstream_url,
180 timeout_seconds
181 );
182 Json(ApiResponse::success("Proxy configuration updated".to_string()))
183}
184
185pub async fn clear_logs(State(_state): State<AdminState>) -> Json<ApiResponse<String>> {
187 if let Some(global_logger) = mockforge_core::get_global_logger() {
188 global_logger.clear_logs().await;
189 }
190 tracing::info!("Request logs cleared via admin UI");
191 Json(ApiResponse::success("Logs cleared".to_string()))
192}
193
194pub async fn restart_servers(State(state): State<super::AdminState>) -> Json<ApiResponse<String>> {
196 let current_status = state.get_restart_status().await;
198 if current_status.in_progress {
199 return Json(ApiResponse::error("Server restart already in progress".to_string()));
200 }
201
202 if let Err(e) = state
204 .initiate_restart("Manual restart requested via admin UI".to_string())
205 .await
206 {
207 return Json(ApiResponse::error(format!("Failed to initiate restart: {}", e)));
208 }
209
210 let state_clone = state.clone();
212 tokio::spawn(async move {
213 if let Err(e) = super::perform_server_restart(&state_clone).await {
214 tracing::error!("Server restart failed: {}", e);
215 state_clone.complete_restart(false).await;
216 } else {
217 tracing::info!("Server restart completed successfully");
218 state_clone.complete_restart(true).await;
219 }
220 });
221
222 tracing::info!("Server restart initiated via admin UI");
223 Json(ApiResponse::success(
224 "Server restart initiated. Please wait for completion.".to_string(),
225 ))
226}
227
228pub async fn get_restart_status(
230 State(state): State<super::AdminState>,
231) -> Json<ApiResponse<super::RestartStatus>> {
232 let status = state.get_restart_status().await;
233 Json(ApiResponse::success(status))
234}
235
236pub async fn get_config(State(state): State<super::AdminState>) -> Json<ApiResponse<Value>> {
238 let config = state.get_config().await;
239 Json(ApiResponse::success(serde_json::to_value(config).unwrap_or_else(|_| json!({}))))
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn create_test_state() -> super::AdminState {
247 super::AdminState::new(None, None, None, None, false, 8080, None, None, None, None, None)
248 }
249
250 #[test]
253 fn test_request_metrics_default() {
254 let metrics = RequestMetrics::default();
255 assert_eq!(metrics.total_requests, 0);
256 assert_eq!(metrics.active_requests, 0);
257 assert_eq!(metrics.average_response_time, 0.0);
258 assert_eq!(metrics.requests_per_second, 0.0);
259 assert_eq!(metrics.total_errors, 0);
260 }
261
262 #[test]
263 fn test_request_metrics_creation() {
264 let metrics = RequestMetrics {
265 total_requests: 1000,
266 active_requests: 10,
267 average_response_time: 45.5,
268 requests_per_second: 25.0,
269 total_errors: 5,
270 };
271
272 assert_eq!(metrics.total_requests, 1000);
273 assert_eq!(metrics.active_requests, 10);
274 assert!((metrics.average_response_time - 45.5).abs() < 0.001);
275 assert!((metrics.requests_per_second - 25.0).abs() < 0.001);
276 assert_eq!(metrics.total_errors, 5);
277 }
278
279 #[test]
280 fn test_request_metrics_clone() {
281 let metrics = RequestMetrics {
282 total_requests: 500,
283 active_requests: 5,
284 average_response_time: 30.0,
285 requests_per_second: 10.0,
286 total_errors: 2,
287 };
288
289 let cloned = metrics.clone();
290 assert_eq!(cloned.total_requests, 500);
291 assert_eq!(cloned.active_requests, 5);
292 }
293
294 #[test]
295 fn test_request_metrics_debug() {
296 let metrics = RequestMetrics::default();
297 let debug_str = format!("{:?}", metrics);
298 assert!(debug_str.contains("RequestMetrics"));
299 assert!(debug_str.contains("total_requests"));
300 }
301
302 #[tokio::test]
305 async fn test_get_restart_status() {
306 let state = create_test_state();
307 let response = get_restart_status(axum::extract::State(state)).await;
308
309 assert!(response.0.success);
310 }
311
312 #[tokio::test]
313 async fn test_get_config() {
314 let state = create_test_state();
315 let response = get_config(axum::extract::State(state)).await;
316
317 assert!(response.0.success);
318 }
319
320 #[tokio::test]
321 async fn test_get_health() {
322 let response = get_health().await;
323
324 assert_eq!(response.0.status, "healthy");
325 assert!(response.0.issues.is_empty());
326 }
327
328 #[tokio::test]
329 async fn test_get_server_info() {
330 let state = create_test_state();
331 let response = get_server_info(axum::extract::State(state)).await;
332
333 assert!(response.0.is_object());
334 let obj = response.0.as_object().unwrap();
335 assert!(obj.contains_key("http_server"));
336 assert!(obj.contains_key("ws_server"));
337 assert!(obj.contains_key("grpc_server"));
338 assert!(obj.contains_key("graphql_server"));
339 assert!(obj.contains_key("api_enabled"));
340 }
341
342 #[tokio::test]
343 async fn test_get_server_info_disabled() {
344 let state = create_test_state();
345 let response = get_server_info(axum::extract::State(state)).await;
346
347 let obj = response.0.as_object().unwrap();
349 assert_eq!(obj.get("http_server").and_then(|v| v.as_str()), Some("disabled"));
350 assert_eq!(obj.get("ws_server").and_then(|v| v.as_str()), Some("disabled"));
351 }
352
353 #[tokio::test]
354 async fn test_get_metrics() {
355 let state = create_test_state();
356 let response = get_metrics(axum::extract::State(state)).await;
357
358 assert!(response.0.success);
359 }
360
361 #[tokio::test]
362 async fn test_get_logs_empty() {
363 let state = create_test_state();
364 let params = HashMap::new();
365 let response = get_logs(axum::extract::State(state), axum::extract::Query(params)).await;
366
367 assert!(response.0.success);
368 }
369
370 #[tokio::test]
371 async fn test_get_logs_with_limit() {
372 let state = create_test_state();
373 let mut params = HashMap::new();
374 params.insert("limit".to_string(), "10".to_string());
375
376 let response = get_logs(axum::extract::State(state), axum::extract::Query(params)).await;
377
378 assert!(response.0.success);
379 }
380
381 #[tokio::test]
382 async fn test_get_logs_with_method_filter() {
383 let state = create_test_state();
384 let mut params = HashMap::new();
385 params.insert("method".to_string(), "GET".to_string());
386
387 let response = get_logs(axum::extract::State(state), axum::extract::Query(params)).await;
388
389 assert!(response.0.success);
390 }
391
392 #[tokio::test]
393 async fn test_get_logs_with_path_filter() {
394 let state = create_test_state();
395 let mut params = HashMap::new();
396 params.insert("path".to_string(), "/api".to_string());
397
398 let response = get_logs(axum::extract::State(state), axum::extract::Query(params)).await;
399
400 assert!(response.0.success);
401 }
402
403 #[tokio::test]
404 async fn test_get_logs_with_status_filter() {
405 let state = create_test_state();
406 let mut params = HashMap::new();
407 params.insert("status".to_string(), "200".to_string());
408
409 let response = get_logs(axum::extract::State(state), axum::extract::Query(params)).await;
410
411 assert!(response.0.success);
412 }
413
414 #[tokio::test]
415 async fn test_clear_logs() {
416 let state = create_test_state();
417 let response = clear_logs(axum::extract::State(state)).await;
418
419 assert!(response.0.success);
420 assert!(response.0.data.is_some());
421 }
422
423 #[tokio::test]
424 async fn test_update_latency() {
425 let state = create_test_state();
426 let config = json!({
427 "base_ms": 100,
428 "jitter_ms": 20
429 });
430
431 let response =
432 update_latency(axum::extract::State(state), axum::extract::Json(config)).await;
433
434 assert!(response.0.success);
435 }
436
437 #[tokio::test]
438 async fn test_update_latency_with_overrides() {
439 let state = create_test_state();
440 let config = json!({
441 "base_ms": 50,
442 "jitter_ms": 10,
443 "tag_overrides": {
444 "slow": 500,
445 "fast": 10
446 }
447 });
448
449 let response =
450 update_latency(axum::extract::State(state), axum::extract::Json(config)).await;
451
452 assert!(response.0.success);
453 }
454
455 #[tokio::test]
456 async fn test_update_faults() {
457 let state = create_test_state();
458 let config = json!({
459 "enabled": true,
460 "failure_rate": 0.1,
461 "status_codes": [500, 503]
462 });
463
464 let response =
465 update_faults(axum::extract::State(state), axum::extract::Json(config)).await;
466
467 assert!(response.0.success);
468 }
469
470 #[tokio::test]
471 async fn test_update_faults_disabled() {
472 let state = create_test_state();
473 let config = json!({
474 "enabled": false
475 });
476
477 let response =
478 update_faults(axum::extract::State(state), axum::extract::Json(config)).await;
479
480 assert!(response.0.success);
481 }
482
483 #[tokio::test]
484 async fn test_update_proxy() {
485 let state = create_test_state();
486 let config = json!({
487 "enabled": true,
488 "upstream_url": "http://localhost:8000",
489 "timeout_seconds": 60
490 });
491
492 let response = update_proxy(axum::extract::State(state), axum::extract::Json(config)).await;
493
494 assert!(response.0.success);
495 }
496
497 #[tokio::test]
498 async fn test_update_proxy_disabled() {
499 let state = create_test_state();
500 let config = json!({
501 "enabled": false
502 });
503
504 let response = update_proxy(axum::extract::State(state), axum::extract::Json(config)).await;
505
506 assert!(response.0.success);
507 }
508
509 #[tokio::test]
510 async fn test_restart_servers() {
511 let state = create_test_state();
512 let response = restart_servers(axum::extract::State(state)).await;
513
514 assert!(response.0.success || response.0.error.is_some());
516 }
517}