1use axum::{
6 extract::{Json, Path},
7 http::StatusCode,
8 response::Json as ResponseJson,
9};
10use mockforge_core::failure_analysis::{FailureContextCollector, FailureNarrativeGenerator};
11use mockforge_core::intelligent_behavior::IntelligentBehaviorConfig;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15use std::sync::Arc;
16use tokio::sync::RwLock;
17use uuid::Uuid;
18
19use crate::models::ApiResponse;
20
21type FailureStorage = Arc<RwLock<HashMap<String, StoredFailure>>>;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27struct StoredFailure {
28 request_id: String,
30 context: mockforge_core::FailureContext,
32 narrative: Option<mockforge_core::FailureNarrative>,
34 timestamp: chrono::DateTime<chrono::Utc>,
36}
37
38static FAILURE_STORAGE: once_cell::sync::Lazy<FailureStorage> =
40 once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AnalyzeFailureRequest {
45 pub method: String,
47 pub path: String,
49 #[serde(default)]
51 pub headers: HashMap<String, String>,
52 #[serde(default)]
54 pub query_params: HashMap<String, String>,
55 pub body: Option<Value>,
57 pub status_code: Option<u16>,
59 #[serde(default)]
61 pub response_headers: HashMap<String, String>,
62 pub response_body: Option<Value>,
64 pub duration_ms: Option<u64>,
66 pub error_message: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct AnalyzeFailureResponse {
73 pub request_id: String,
75 pub context: mockforge_core::FailureContext,
77 pub narrative: Option<mockforge_core::FailureNarrative>,
79 pub error: Option<String>,
81}
82
83pub async fn analyze_failure(
87 Json(request): Json<AnalyzeFailureRequest>,
88) -> Result<ResponseJson<ApiResponse<AnalyzeFailureResponse>>, StatusCode> {
89 let request_id = Uuid::new_v4().to_string();
91
92 let collector = FailureContextCollector::new();
94
95 let context = collector
97 .collect_context_with_details(
98 &request.method,
99 &request.path,
100 request.headers,
101 request.query_params,
102 request.body,
103 request.status_code,
104 request.response_headers,
105 request.response_body,
106 request.duration_ms,
107 request.error_message,
108 vec![], vec![], None, vec![], vec![], )
114 .map_err(|e| {
115 tracing::error!("Failed to collect failure context: {}", e);
116 StatusCode::INTERNAL_SERVER_ERROR
117 })?;
118
119 let config = IntelligentBehaviorConfig::default();
121 let generator = FailureNarrativeGenerator::new(config);
122 let narrative = match generator.generate_narrative(&context).await {
123 Ok(narrative) => Some(narrative),
124 Err(e) => {
125 tracing::warn!("Failed to generate narrative: {}", e);
126 None
127 }
128 };
129
130 let stored = StoredFailure {
132 request_id: request_id.clone(),
133 context: context.clone(),
134 narrative: narrative.clone(),
135 timestamp: chrono::Utc::now(),
136 };
137
138 {
139 let mut storage = FAILURE_STORAGE.write().await;
140 storage.insert(request_id.clone(), stored);
141 }
142
143 let response = AnalyzeFailureResponse {
144 request_id,
145 context,
146 narrative,
147 error: None,
148 };
149
150 Ok(ResponseJson(ApiResponse::success(response)))
151}
152
153pub async fn get_failure_analysis(
157 Path(request_id): Path<String>,
158) -> Result<ResponseJson<ApiResponse<AnalyzeFailureResponse>>, StatusCode> {
159 let storage = FAILURE_STORAGE.read().await;
160 let stored = storage.get(&request_id).ok_or(StatusCode::NOT_FOUND)?;
161
162 let response = AnalyzeFailureResponse {
163 request_id: stored.request_id.clone(),
164 context: stored.context.clone(),
165 narrative: stored.narrative.clone(),
166 error: None,
167 };
168
169 Ok(ResponseJson(ApiResponse::success(response)))
170}
171
172pub async fn list_recent_failures(
176) -> Result<ResponseJson<ApiResponse<Vec<FailureSummary>>>, StatusCode> {
177 let storage = FAILURE_STORAGE.read().await;
178
179 let mut failures: Vec<_> = storage
181 .values()
182 .map(|f| FailureSummary {
183 request_id: f.request_id.clone(),
184 method: f.context.request.method.clone(),
185 path: f.context.request.path.clone(),
186 status_code: f.context.response.as_ref().map(|r| r.status_code),
187 error_message: f.context.error_message.clone(),
188 timestamp: f.timestamp,
189 has_narrative: f.narrative.is_some(),
190 })
191 .collect();
192
193 failures.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
194 failures.truncate(50); Ok(ResponseJson(ApiResponse::success(failures)))
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct FailureSummary {
202 pub request_id: String,
204 pub method: String,
206 pub path: String,
208 pub status_code: Option<u16>,
210 pub error_message: Option<String>,
212 pub timestamp: chrono::DateTime<chrono::Utc>,
214 pub has_narrative: bool,
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
225 fn test_analyze_failure_request_minimal() {
226 let request = AnalyzeFailureRequest {
227 method: "GET".to_string(),
228 path: "/api/users".to_string(),
229 headers: HashMap::new(),
230 query_params: HashMap::new(),
231 body: None,
232 status_code: None,
233 response_headers: HashMap::new(),
234 response_body: None,
235 duration_ms: None,
236 error_message: None,
237 };
238
239 assert_eq!(request.method, "GET");
240 assert_eq!(request.path, "/api/users");
241 assert!(request.headers.is_empty());
242 }
243
244 #[test]
245 fn test_analyze_failure_request_full() {
246 let mut headers = HashMap::new();
247 headers.insert("Content-Type".to_string(), "application/json".to_string());
248 headers.insert("Authorization".to_string(), "Bearer token".to_string());
249
250 let mut query_params = HashMap::new();
251 query_params.insert("page".to_string(), "1".to_string());
252
253 let request = AnalyzeFailureRequest {
254 method: "POST".to_string(),
255 path: "/api/orders".to_string(),
256 headers,
257 query_params,
258 body: Some(serde_json::json!({"item": "book", "quantity": 1})),
259 status_code: Some(500),
260 response_headers: HashMap::new(),
261 response_body: Some(serde_json::json!({"error": "Internal Server Error"})),
262 duration_ms: Some(1500),
263 error_message: Some("Database connection failed".to_string()),
264 };
265
266 assert_eq!(request.method, "POST");
267 assert_eq!(request.status_code, Some(500));
268 assert_eq!(request.duration_ms, Some(1500));
269 assert!(request.error_message.is_some());
270 }
271
272 #[test]
273 fn test_analyze_failure_request_serialization() {
274 let request = AnalyzeFailureRequest {
275 method: "DELETE".to_string(),
276 path: "/api/items/123".to_string(),
277 headers: HashMap::new(),
278 query_params: HashMap::new(),
279 body: None,
280 status_code: Some(404),
281 response_headers: HashMap::new(),
282 response_body: None,
283 duration_ms: Some(50),
284 error_message: Some("Not found".to_string()),
285 };
286
287 let json = serde_json::to_string(&request).unwrap();
288 assert!(json.contains("DELETE"));
289 assert!(json.contains("/api/items/123"));
290 assert!(json.contains("404"));
291 }
292
293 #[test]
294 fn test_analyze_failure_request_deserialization() {
295 let json = r#"{
296 "method": "PUT",
297 "path": "/api/profile",
298 "headers": {"Content-Type": "application/json"},
299 "query_params": {},
300 "body": {"name": "Test"},
301 "status_code": 400,
302 "response_headers": {},
303 "response_body": {"error": "Validation failed"},
304 "duration_ms": 100,
305 "error_message": "Invalid input"
306 }"#;
307
308 let request: AnalyzeFailureRequest = serde_json::from_str(json).unwrap();
309 assert_eq!(request.method, "PUT");
310 assert_eq!(request.path, "/api/profile");
311 assert_eq!(request.status_code, Some(400));
312 assert_eq!(request.error_message, Some("Invalid input".to_string()));
313 }
314
315 #[test]
316 fn test_analyze_failure_request_clone() {
317 let request = AnalyzeFailureRequest {
318 method: "GET".to_string(),
319 path: "/test".to_string(),
320 headers: HashMap::new(),
321 query_params: HashMap::new(),
322 body: None,
323 status_code: Some(200),
324 response_headers: HashMap::new(),
325 response_body: None,
326 duration_ms: Some(10),
327 error_message: None,
328 };
329
330 let cloned = request.clone();
331 assert_eq!(cloned.method, request.method);
332 assert_eq!(cloned.path, request.path);
333 }
334
335 fn create_test_failure_context() -> mockforge_core::FailureContext {
338 mockforge_core::FailureContext {
339 request: mockforge_core::failure_analysis::RequestDetails {
340 method: "GET".to_string(),
341 path: "/test".to_string(),
342 headers: HashMap::new(),
343 query_params: HashMap::new(),
344 body: None,
345 },
346 response: None,
347 chaos_configs: vec![],
348 consistency_rules: vec![],
349 contract_validation: None,
350 behavioral_rules: vec![],
351 hook_results: vec![],
352 error_message: None,
353 timestamp: chrono::Utc::now(),
354 }
355 }
356
357 #[test]
358 fn test_analyze_failure_response_creation() {
359 let response = AnalyzeFailureResponse {
360 request_id: "test-uuid-123".to_string(),
361 context: create_test_failure_context(),
362 narrative: None,
363 error: None,
364 };
365
366 assert_eq!(response.request_id, "test-uuid-123");
367 assert!(response.narrative.is_none());
368 assert!(response.error.is_none());
369 }
370
371 #[test]
372 fn test_analyze_failure_response_with_error() {
373 let response = AnalyzeFailureResponse {
374 request_id: "test-123".to_string(),
375 context: create_test_failure_context(),
376 narrative: None,
377 error: Some("Analysis failed: timeout".to_string()),
378 };
379
380 assert!(response.error.is_some());
381 assert_eq!(response.error.unwrap(), "Analysis failed: timeout");
382 }
383
384 #[test]
385 fn test_analyze_failure_response_serialization() {
386 let response = AnalyzeFailureResponse {
387 request_id: "uuid-456".to_string(),
388 context: create_test_failure_context(),
389 narrative: None,
390 error: None,
391 };
392
393 let json = serde_json::to_string(&response).unwrap();
394 assert!(json.contains("uuid-456"));
395 assert!(json.contains("request_id"));
396 }
397
398 #[test]
399 fn test_analyze_failure_response_clone() {
400 let response = AnalyzeFailureResponse {
401 request_id: "clone-test".to_string(),
402 context: create_test_failure_context(),
403 narrative: None,
404 error: Some("Test error".to_string()),
405 };
406
407 let cloned = response.clone();
408 assert_eq!(cloned.request_id, response.request_id);
409 assert_eq!(cloned.error, response.error);
410 }
411
412 #[test]
415 fn test_failure_summary_creation() {
416 let summary = FailureSummary {
417 request_id: "summary-123".to_string(),
418 method: "GET".to_string(),
419 path: "/api/test".to_string(),
420 status_code: Some(500),
421 error_message: Some("Internal error".to_string()),
422 timestamp: chrono::Utc::now(),
423 has_narrative: true,
424 };
425
426 assert_eq!(summary.request_id, "summary-123");
427 assert_eq!(summary.method, "GET");
428 assert!(summary.has_narrative);
429 }
430
431 #[test]
432 fn test_failure_summary_no_status_code() {
433 let summary = FailureSummary {
434 request_id: "no-status".to_string(),
435 method: "POST".to_string(),
436 path: "/api/action".to_string(),
437 status_code: None,
438 error_message: Some("Connection timeout".to_string()),
439 timestamp: chrono::Utc::now(),
440 has_narrative: false,
441 };
442
443 assert!(summary.status_code.is_none());
444 assert!(!summary.has_narrative);
445 }
446
447 #[test]
448 fn test_failure_summary_serialization() {
449 let summary = FailureSummary {
450 request_id: "serialize-test".to_string(),
451 method: "DELETE".to_string(),
452 path: "/api/item/1".to_string(),
453 status_code: Some(403),
454 error_message: Some("Forbidden".to_string()),
455 timestamp: chrono::DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
456 .unwrap()
457 .with_timezone(&chrono::Utc),
458 has_narrative: true,
459 };
460
461 let json = serde_json::to_string(&summary).unwrap();
462 assert!(json.contains("serialize-test"));
463 assert!(json.contains("DELETE"));
464 assert!(json.contains("403"));
465 assert!(json.contains("Forbidden"));
466 }
467
468 #[test]
469 fn test_failure_summary_deserialization() {
470 let json = r#"{
471 "request_id": "deser-test",
472 "method": "PUT",
473 "path": "/api/update",
474 "status_code": 422,
475 "error_message": "Unprocessable Entity",
476 "timestamp": "2024-01-15T12:00:00Z",
477 "has_narrative": false
478 }"#;
479
480 let summary: FailureSummary = serde_json::from_str(json).unwrap();
481 assert_eq!(summary.request_id, "deser-test");
482 assert_eq!(summary.method, "PUT");
483 assert_eq!(summary.status_code, Some(422));
484 assert!(!summary.has_narrative);
485 }
486
487 #[test]
488 fn test_failure_summary_clone() {
489 let summary = FailureSummary {
490 request_id: "clone-test".to_string(),
491 method: "PATCH".to_string(),
492 path: "/api/partial".to_string(),
493 status_code: Some(200),
494 error_message: None,
495 timestamp: chrono::Utc::now(),
496 has_narrative: true,
497 };
498
499 let cloned = summary.clone();
500 assert_eq!(cloned.request_id, summary.request_id);
501 assert_eq!(cloned.method, summary.method);
502 assert_eq!(cloned.has_narrative, summary.has_narrative);
503 }
504
505 #[test]
506 fn test_failure_summary_debug() {
507 let summary = FailureSummary {
508 request_id: "debug-test".to_string(),
509 method: "GET".to_string(),
510 path: "/debug".to_string(),
511 status_code: Some(200),
512 error_message: None,
513 timestamp: chrono::Utc::now(),
514 has_narrative: false,
515 };
516
517 let debug = format!("{:?}", summary);
518 assert!(debug.contains("debug-test"));
519 assert!(debug.contains("GET"));
520 }
521
522 #[test]
525 fn test_analyze_failure_request_with_complex_body() {
526 let body = serde_json::json!({
527 "user": {
528 "name": "John",
529 "roles": ["admin", "user"],
530 "metadata": {
531 "created": "2024-01-01"
532 }
533 },
534 "items": [1, 2, 3]
535 });
536
537 let request = AnalyzeFailureRequest {
538 method: "POST".to_string(),
539 path: "/api/complex".to_string(),
540 headers: HashMap::new(),
541 query_params: HashMap::new(),
542 body: Some(body.clone()),
543 status_code: Some(201),
544 response_headers: HashMap::new(),
545 response_body: None,
546 duration_ms: Some(500),
547 error_message: None,
548 };
549
550 assert!(request.body.is_some());
551 let body_value = request.body.unwrap();
552 assert!(body_value.get("user").is_some());
553 }
554
555 #[test]
556 fn test_failure_summary_various_http_methods() {
557 let methods = vec!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
558
559 for method in methods {
560 let summary = FailureSummary {
561 request_id: format!("method-{}", method),
562 method: method.to_string(),
563 path: "/test".to_string(),
564 status_code: Some(200),
565 error_message: None,
566 timestamp: chrono::Utc::now(),
567 has_narrative: false,
568 };
569
570 assert_eq!(summary.method, method);
571 }
572 }
573
574 #[test]
575 fn test_failure_summary_various_status_codes() {
576 let status_codes = vec![200, 201, 400, 401, 403, 404, 500, 502, 503];
577
578 for code in status_codes {
579 let summary = FailureSummary {
580 request_id: format!("status-{}", code),
581 method: "GET".to_string(),
582 path: "/test".to_string(),
583 status_code: Some(code),
584 error_message: None,
585 timestamp: chrono::Utc::now(),
586 has_narrative: false,
587 };
588
589 assert_eq!(summary.status_code, Some(code));
590 }
591 }
592}