1use axum::{
10 extract::{Path, Query},
11 http::StatusCode,
12 response::{IntoResponse, Response},
13 Json,
14};
15use mockforge_core::{
16 ai_contract_diff::{CapturedRequest, ContractDiffAnalyzer, ContractDiffConfig},
17 openapi::OpenApiSpec,
18 request_capture::{get_global_capture_manager, CaptureQuery},
19 Error,
20};
21use serde::Deserialize;
22use serde_json::json;
23use std::collections::HashMap;
24
25fn error_response(error: Error) -> Response {
27 (
28 StatusCode::INTERNAL_SERVER_ERROR,
29 Json(json!({
30 "success": false,
31 "error": error.to_string()
32 })),
33 )
34 .into_response()
35}
36
37pub async fn upload_request(Json(payload): Json<UploadRequestPayload>) -> impl IntoResponse {
39 let request = CapturedRequest::new(&payload.method, &payload.path, "manual_upload")
40 .with_headers(payload.headers.unwrap_or_default())
41 .with_query_params(payload.query_params.unwrap_or_default());
42
43 let request = if let Some(body) = payload.body {
44 request.with_body(body)
45 } else {
46 request
47 };
48
49 let request = if let Some(status_code) = payload.status_code {
50 request.with_response(status_code, payload.response_body)
51 } else {
52 request
53 };
54
55 let capture_manager = match get_global_capture_manager() {
57 Some(manager) => manager,
58 None => {
59 return (
60 StatusCode::INTERNAL_SERVER_ERROR,
61 Json(json!({
62 "success": false,
63 "error": "Capture manager not initialized"
64 })),
65 )
66 .into_response();
67 }
68 };
69
70 match capture_manager.capture(request).await {
71 Ok(capture_id) => (
72 StatusCode::OK,
73 Json(json!({
74 "success": true,
75 "capture_id": capture_id,
76 "message": "Request captured successfully"
77 })),
78 )
79 .into_response(),
80 Err(e) => (
81 StatusCode::INTERNAL_SERVER_ERROR,
82 Json(json!({
83 "success": false,
84 "error": e.to_string()
85 })),
86 )
87 .into_response(),
88 }
89}
90
91pub async fn submit_request(Json(payload): Json<SubmitRequestPayload>) -> impl IntoResponse {
93 let request = CapturedRequest::new(&payload.method, &payload.path, "api_endpoint")
94 .with_headers(payload.headers.unwrap_or_default())
95 .with_query_params(payload.query_params.unwrap_or_default());
96
97 let request = if let Some(body) = payload.body {
98 request.with_body(body)
99 } else {
100 request
101 };
102
103 let request = if let Some(status_code) = payload.status_code {
104 request.with_response(status_code, payload.response_body)
105 } else {
106 request
107 };
108
109 let capture_manager = match get_global_capture_manager() {
111 Some(manager) => manager,
112 None => {
113 return (
114 StatusCode::INTERNAL_SERVER_ERROR,
115 Json(json!({
116 "success": false,
117 "error": "Capture manager not initialized"
118 })),
119 )
120 .into_response();
121 }
122 };
123
124 match capture_manager.capture(request).await {
125 Ok(capture_id) => (
126 StatusCode::OK,
127 Json(json!({
128 "success": true,
129 "capture_id": capture_id,
130 "message": "Request submitted successfully"
131 })),
132 )
133 .into_response(),
134 Err(e) => error_response(e),
135 }
136}
137
138pub async fn get_captured_requests(
140 Query(params): Query<HashMap<String, String>>,
141) -> impl IntoResponse {
142 let capture_manager = match get_global_capture_manager() {
143 Some(manager) => manager,
144 None => {
145 return (
146 StatusCode::INTERNAL_SERVER_ERROR,
147 Json(json!({
148 "success": false,
149 "error": "Capture manager not initialized"
150 })),
151 )
152 .into_response();
153 }
154 };
155
156 let query = CaptureQuery {
157 source: params.get("source").cloned(),
158 method: params.get("method").cloned(),
159 path_pattern: params.get("path_pattern").cloned(),
160 analyzed: params.get("analyzed").and_then(|s| s.parse().ok()),
161 limit: params.get("limit").and_then(|s| s.parse().ok()),
162 offset: params.get("offset").and_then(|s| s.parse().ok()),
163 ..Default::default()
164 };
165
166 let captures = capture_manager.query_captures(query).await;
167
168 (
169 StatusCode::OK,
170 Json(json!({
171 "success": true,
172 "count": captures.len(),
173 "captures": captures.iter().map(|(req, meta)| json!({
174 "id": meta.id,
175 "method": req.method,
176 "path": req.path,
177 "source": meta.source,
178 "captured_at": meta.captured_at,
179 "analyzed": meta.analyzed,
180 "query_params": req.query_params,
181 "headers": req.headers,
182 })).collect::<Vec<_>>()
183 })),
184 )
185 .into_response()
186}
187
188pub async fn get_captured_request(Path(capture_id): Path<String>) -> impl IntoResponse {
190 let capture_manager = match get_global_capture_manager() {
191 Some(manager) => manager,
192 None => {
193 return (
194 StatusCode::INTERNAL_SERVER_ERROR,
195 Json(json!({
196 "success": false,
197 "error": "Capture manager not initialized"
198 })),
199 )
200 .into_response();
201 }
202 };
203
204 let (request, metadata) = match capture_manager.get_capture(&capture_id).await {
205 Some(result) => result,
206 None => {
207 return (
208 StatusCode::NOT_FOUND,
209 Json(json!({
210 "success": false,
211 "error": format!("Capture not found: {}", capture_id)
212 })),
213 )
214 .into_response();
215 }
216 };
217
218 (
219 StatusCode::OK,
220 Json(json!({
221 "success": true,
222 "capture": {
223 "id": metadata.id,
224 "method": request.method,
225 "path": request.path,
226 "source": metadata.source,
227 "captured_at": metadata.captured_at,
228 "analyzed": metadata.analyzed,
229 "contract_id": metadata.contract_id,
230 "analysis_result_id": metadata.analysis_result_id,
231 "query_params": request.query_params,
232 "headers": request.headers,
233 "body": request.body,
234 "status_code": request.status_code,
235 "response_body": request.response_body,
236 "user_agent": request.user_agent,
237 "metadata": request.metadata,
238 }
239 })),
240 )
241 .into_response()
242}
243
244pub async fn analyze_captured_request(
246 Path(capture_id): Path<String>,
247 Json(payload): Json<AnalyzeRequestPayload>,
248) -> impl IntoResponse {
249 let capture_manager = match get_global_capture_manager() {
250 Some(manager) => manager,
251 None => {
252 return (
253 StatusCode::INTERNAL_SERVER_ERROR,
254 Json(json!({
255 "success": false,
256 "error": "Capture manager not initialized"
257 })),
258 )
259 .into_response();
260 }
261 };
262
263 let (request, _metadata) = match capture_manager.get_capture(&capture_id).await {
265 Some(result) => result,
266 None => {
267 return (
268 StatusCode::NOT_FOUND,
269 Json(json!({
270 "success": false,
271 "error": format!("Capture not found: {}", capture_id)
272 })),
273 )
274 .into_response();
275 }
276 };
277
278 let spec = match if let Some(spec_path) = &payload.spec_path {
280 OpenApiSpec::from_file(spec_path).await
281 } else if let Some(spec_content) = &payload.spec_content {
282 let format = if spec_content.trim_start().starts_with('{') {
284 None } else {
286 Some("yaml") };
288 OpenApiSpec::from_string(spec_content, format)
289 } else {
290 return (
291 StatusCode::BAD_REQUEST,
292 Json(json!({
293 "success": false,
294 "error": "Either spec_path or spec_content must be provided"
295 })),
296 )
297 .into_response();
298 } {
299 Ok(spec) => spec,
300 Err(e) => return error_response(e),
301 };
302
303 let config = payload.config.unwrap_or_else(ContractDiffConfig::default);
305 let analyzer = match ContractDiffAnalyzer::new(config) {
306 Ok(analyzer) => analyzer,
307 Err(e) => return error_response(e),
308 };
309
310 let result = match analyzer.analyze(&request, &spec).await {
312 Ok(result) => result,
313 Err(e) => return error_response(e),
314 };
315
316 let analysis_result_id = uuid::Uuid::new_v4().to_string();
318 let contract_id = payload.contract_id.unwrap_or_else(|| "default".to_string());
319 if let Err(e) = capture_manager
320 .mark_analyzed(&capture_id, &contract_id, &analysis_result_id)
321 .await
322 {
323 return error_response(e);
324 }
325
326 (
327 StatusCode::OK,
328 Json(json!({
329 "success": true,
330 "analysis_result_id": analysis_result_id,
331 "result": result
332 })),
333 )
334 .into_response()
335}
336
337pub async fn get_capture_statistics() -> impl IntoResponse {
339 let capture_manager = match get_global_capture_manager() {
340 Some(manager) => manager,
341 None => {
342 return (
343 StatusCode::INTERNAL_SERVER_ERROR,
344 Json(json!({
345 "success": false,
346 "error": "Capture manager not initialized"
347 })),
348 )
349 .into_response();
350 }
351 };
352
353 let stats = capture_manager.get_statistics().await;
354
355 (
356 StatusCode::OK,
357 Json(json!({
358 "success": true,
359 "statistics": stats
360 })),
361 )
362 .into_response()
363}
364
365pub async fn generate_patch_file(
367 Path(capture_id): Path<String>,
368 Json(payload): Json<GeneratePatchPayload>,
369) -> impl IntoResponse {
370 let capture_manager = match get_global_capture_manager() {
371 Some(manager) => manager,
372 None => {
373 return (
374 StatusCode::INTERNAL_SERVER_ERROR,
375 Json(json!({
376 "success": false,
377 "error": "Capture manager not initialized"
378 })),
379 )
380 .into_response();
381 }
382 };
383
384 let (request, _metadata) = match capture_manager.get_capture(&capture_id).await {
386 Some(result) => result,
387 None => {
388 return (
389 StatusCode::NOT_FOUND,
390 Json(json!({
391 "success": false,
392 "error": format!("Capture not found: {}", capture_id)
393 })),
394 )
395 .into_response();
396 }
397 };
398
399 let spec = match if let Some(spec_path) = &payload.spec_path {
401 OpenApiSpec::from_file(spec_path).await
402 } else if let Some(spec_content) = &payload.spec_content {
403 let format = if spec_content.trim_start().starts_with('{') {
404 None } else {
406 Some("yaml") };
408 OpenApiSpec::from_string(spec_content, format)
409 } else {
410 return (
411 StatusCode::BAD_REQUEST,
412 Json(json!({
413 "success": false,
414 "error": "Either spec_path or spec_content must be provided"
415 })),
416 )
417 .into_response();
418 } {
419 Ok(spec) => spec,
420 Err(e) => return error_response(e),
421 };
422
423 let config = payload.config.unwrap_or_else(ContractDiffConfig::default);
425 let analyzer = match ContractDiffAnalyzer::new(config) {
426 Ok(analyzer) => analyzer,
427 Err(e) => return error_response(e),
428 };
429
430 let result = match analyzer.analyze(&request, &spec).await {
432 Ok(result) => result,
433 Err(e) => return error_response(e),
434 };
435
436 if result.corrections.is_empty() {
438 return (
439 StatusCode::BAD_REQUEST,
440 Json(json!({
441 "success": false,
442 "error": "No corrections available to generate patch"
443 })),
444 )
445 .into_response();
446 }
447
448 let spec_version = if spec.spec.info.version.is_empty() {
449 "1.0.0".to_string()
450 } else {
451 spec.spec.info.version.clone()
452 };
453 let patch_file = analyzer.generate_patch_file(&result.corrections, &spec_version);
454
455 (
456 StatusCode::OK,
457 Json(json!({
458 "success": true,
459 "patch_file": patch_file,
460 "corrections_count": result.corrections.len()
461 })),
462 )
463 .into_response()
464}
465
466#[derive(Debug, Deserialize)]
468pub struct GeneratePatchPayload {
469 pub spec_path: Option<String>,
471
472 pub spec_content: Option<String>,
474
475 pub config: Option<ContractDiffConfig>,
477}
478
479#[derive(Debug, Deserialize)]
481pub struct UploadRequestPayload {
482 pub method: String,
483 pub path: String,
484 pub headers: Option<HashMap<String, String>>,
485 pub query_params: Option<HashMap<String, String>>,
486 pub body: Option<serde_json::Value>,
487 pub status_code: Option<u16>,
488 pub response_body: Option<serde_json::Value>,
489}
490
491#[derive(Debug, Deserialize)]
493pub struct SubmitRequestPayload {
494 pub method: String,
495 pub path: String,
496 pub headers: Option<HashMap<String, String>>,
497 pub query_params: Option<HashMap<String, String>>,
498 pub body: Option<serde_json::Value>,
499 pub status_code: Option<u16>,
500 pub response_body: Option<serde_json::Value>,
501}
502
503#[derive(Debug, Deserialize)]
505pub struct AnalyzeRequestPayload {
506 pub spec_path: Option<String>,
508
509 pub spec_content: Option<String>,
511
512 pub contract_id: Option<String>,
514
515 pub config: Option<ContractDiffConfig>,
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
526 fn test_upload_request_payload_minimal() {
527 let payload = UploadRequestPayload {
528 method: "GET".to_string(),
529 path: "/api/users".to_string(),
530 headers: None,
531 query_params: None,
532 body: None,
533 status_code: None,
534 response_body: None,
535 };
536
537 assert_eq!(payload.method, "GET");
538 assert_eq!(payload.path, "/api/users");
539 assert!(payload.headers.is_none());
540 }
541
542 #[test]
543 fn test_upload_request_payload_full() {
544 let mut headers = HashMap::new();
545 headers.insert("Content-Type".to_string(), "application/json".to_string());
546
547 let mut query_params = HashMap::new();
548 query_params.insert("page".to_string(), "1".to_string());
549
550 let payload = UploadRequestPayload {
551 method: "POST".to_string(),
552 path: "/api/orders".to_string(),
553 headers: Some(headers),
554 query_params: Some(query_params),
555 body: Some(serde_json::json!({"item": "book"})),
556 status_code: Some(201),
557 response_body: Some(serde_json::json!({"id": 123})),
558 };
559
560 assert_eq!(payload.method, "POST");
561 assert_eq!(payload.status_code, Some(201));
562 assert!(payload.body.is_some());
563 }
564
565 #[test]
566 fn test_upload_request_payload_deserialization() {
567 let json = r#"{
568 "method": "DELETE",
569 "path": "/api/items/123",
570 "status_code": 204
571 }"#;
572
573 let payload: UploadRequestPayload = serde_json::from_str(json).unwrap();
574 assert_eq!(payload.method, "DELETE");
575 assert_eq!(payload.path, "/api/items/123");
576 assert_eq!(payload.status_code, Some(204));
577 }
578
579 #[test]
580 fn test_upload_request_payload_debug() {
581 let payload = UploadRequestPayload {
582 method: "GET".to_string(),
583 path: "/test".to_string(),
584 headers: None,
585 query_params: None,
586 body: None,
587 status_code: None,
588 response_body: None,
589 };
590
591 let debug = format!("{:?}", payload);
592 assert!(debug.contains("GET"));
593 assert!(debug.contains("/test"));
594 }
595
596 #[test]
599 fn test_submit_request_payload_minimal() {
600 let payload = SubmitRequestPayload {
601 method: "PUT".to_string(),
602 path: "/api/update".to_string(),
603 headers: None,
604 query_params: None,
605 body: None,
606 status_code: None,
607 response_body: None,
608 };
609
610 assert_eq!(payload.method, "PUT");
611 assert_eq!(payload.path, "/api/update");
612 }
613
614 #[test]
615 fn test_submit_request_payload_with_body() {
616 let payload = SubmitRequestPayload {
617 method: "POST".to_string(),
618 path: "/api/data".to_string(),
619 headers: None,
620 query_params: None,
621 body: Some(serde_json::json!({"key": "value"})),
622 status_code: Some(200),
623 response_body: Some(serde_json::json!({"success": true})),
624 };
625
626 assert!(payload.body.is_some());
627 assert!(payload.response_body.is_some());
628 }
629
630 #[test]
631 fn test_submit_request_payload_deserialization() {
632 let json = r#"{
633 "method": "PATCH",
634 "path": "/api/partial",
635 "body": {"field": "updated"}
636 }"#;
637
638 let payload: SubmitRequestPayload = serde_json::from_str(json).unwrap();
639 assert_eq!(payload.method, "PATCH");
640 assert!(payload.body.is_some());
641 }
642
643 #[test]
646 fn test_analyze_request_payload_with_spec_path() {
647 let payload = AnalyzeRequestPayload {
648 spec_path: Some("/path/to/spec.yaml".to_string()),
649 spec_content: None,
650 contract_id: Some("contract-123".to_string()),
651 config: None,
652 };
653
654 assert!(payload.spec_path.is_some());
655 assert!(payload.spec_content.is_none());
656 assert_eq!(payload.contract_id, Some("contract-123".to_string()));
657 }
658
659 #[test]
660 fn test_analyze_request_payload_with_spec_content() {
661 let spec_content = r#"
662 openapi: "3.0.0"
663 info:
664 title: Test API
665 version: "1.0.0"
666 "#;
667
668 let payload = AnalyzeRequestPayload {
669 spec_path: None,
670 spec_content: Some(spec_content.to_string()),
671 contract_id: None,
672 config: None,
673 };
674
675 assert!(payload.spec_path.is_none());
676 assert!(payload.spec_content.is_some());
677 }
678
679 #[test]
680 fn test_analyze_request_payload_deserialization() {
681 let json = r#"{
682 "spec_path": "/specs/api.yaml",
683 "contract_id": "my-contract"
684 }"#;
685
686 let payload: AnalyzeRequestPayload = serde_json::from_str(json).unwrap();
687 assert_eq!(payload.spec_path, Some("/specs/api.yaml".to_string()));
688 assert_eq!(payload.contract_id, Some("my-contract".to_string()));
689 }
690
691 #[test]
692 fn test_analyze_request_payload_empty() {
693 let json = r#"{}"#;
694
695 let payload: AnalyzeRequestPayload = serde_json::from_str(json).unwrap();
696 assert!(payload.spec_path.is_none());
697 assert!(payload.spec_content.is_none());
698 assert!(payload.contract_id.is_none());
699 assert!(payload.config.is_none());
700 }
701
702 #[test]
705 fn test_generate_patch_payload_with_spec_path() {
706 let payload = GeneratePatchPayload {
707 spec_path: Some("/path/to/spec.json".to_string()),
708 spec_content: None,
709 config: None,
710 };
711
712 assert!(payload.spec_path.is_some());
713 assert!(payload.spec_content.is_none());
714 }
715
716 #[test]
717 fn test_generate_patch_payload_with_spec_content() {
718 let payload = GeneratePatchPayload {
719 spec_path: None,
720 spec_content: Some("{}".to_string()),
721 config: None,
722 };
723
724 assert!(payload.spec_path.is_none());
725 assert!(payload.spec_content.is_some());
726 }
727
728 #[test]
729 fn test_generate_patch_payload_deserialization() {
730 let json = r#"{
731 "spec_path": "/api/openapi.yaml"
732 }"#;
733
734 let payload: GeneratePatchPayload = serde_json::from_str(json).unwrap();
735 assert_eq!(payload.spec_path, Some("/api/openapi.yaml".to_string()));
736 }
737
738 #[test]
741 fn test_error_response_creation() {
742 let error = Error::validation("test error");
743 let response = error_response(error);
744 let _ = response;
746 }
747
748 #[test]
751 fn test_all_http_methods() {
752 let methods = vec!["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
753
754 for method in methods {
755 let payload = UploadRequestPayload {
756 method: method.to_string(),
757 path: "/test".to_string(),
758 headers: None,
759 query_params: None,
760 body: None,
761 status_code: None,
762 response_body: None,
763 };
764
765 assert_eq!(payload.method, method);
766 }
767 }
768
769 #[test]
770 fn test_various_status_codes() {
771 let status_codes = vec![200, 201, 204, 400, 401, 403, 404, 500, 502, 503];
772
773 for code in status_codes {
774 let payload = UploadRequestPayload {
775 method: "GET".to_string(),
776 path: "/test".to_string(),
777 headers: None,
778 query_params: None,
779 body: None,
780 status_code: Some(code),
781 response_body: None,
782 };
783
784 assert_eq!(payload.status_code, Some(code));
785 }
786 }
787
788 #[test]
791 fn test_payload_with_empty_path() {
792 let payload = UploadRequestPayload {
793 method: "GET".to_string(),
794 path: "".to_string(),
795 headers: None,
796 query_params: None,
797 body: None,
798 status_code: None,
799 response_body: None,
800 };
801
802 assert!(payload.path.is_empty());
803 }
804
805 #[test]
806 fn test_payload_with_complex_body() {
807 let body = serde_json::json!({
808 "user": {
809 "name": "John",
810 "roles": ["admin", "user"],
811 "settings": {
812 "theme": "dark",
813 "notifications": true
814 }
815 },
816 "items": [1, 2, 3, 4, 5]
817 });
818
819 let payload = UploadRequestPayload {
820 method: "POST".to_string(),
821 path: "/api/complex".to_string(),
822 headers: None,
823 query_params: None,
824 body: Some(body),
825 status_code: None,
826 response_body: None,
827 };
828
829 assert!(payload.body.is_some());
830 let body_val = payload.body.unwrap();
831 assert!(body_val.get("user").is_some());
832 assert!(body_val.get("items").is_some());
833 }
834
835 #[test]
836 fn test_payload_with_many_headers() {
837 let mut headers = HashMap::new();
838 headers.insert("Content-Type".to_string(), "application/json".to_string());
839 headers.insert("Authorization".to_string(), "Bearer token123".to_string());
840 headers.insert("X-Request-ID".to_string(), "uuid-123".to_string());
841 headers.insert("Accept".to_string(), "application/json".to_string());
842 headers.insert("X-Custom-Header".to_string(), "custom-value".to_string());
843
844 let payload = UploadRequestPayload {
845 method: "GET".to_string(),
846 path: "/api/test".to_string(),
847 headers: Some(headers.clone()),
848 query_params: None,
849 body: None,
850 status_code: None,
851 response_body: None,
852 };
853
854 assert!(payload.headers.is_some());
855 assert_eq!(payload.headers.unwrap().len(), 5);
856 }
857
858 #[test]
859 fn test_payload_with_many_query_params() {
860 let mut query_params = HashMap::new();
861 query_params.insert("page".to_string(), "1".to_string());
862 query_params.insert("limit".to_string(), "50".to_string());
863 query_params.insert("sort".to_string(), "created_at".to_string());
864 query_params.insert("order".to_string(), "desc".to_string());
865 query_params.insert("filter".to_string(), "active".to_string());
866
867 let payload = UploadRequestPayload {
868 method: "GET".to_string(),
869 path: "/api/list".to_string(),
870 headers: None,
871 query_params: Some(query_params.clone()),
872 body: None,
873 status_code: None,
874 response_body: None,
875 };
876
877 assert!(payload.query_params.is_some());
878 assert_eq!(payload.query_params.unwrap().len(), 5);
879 }
880}