mockforge_ui/handlers/
contract_diff.rs

1//! Contract diff API handlers
2//!
3//! This module provides API endpoints for:
4//! - Manual request upload
5//! - Programmatic request submission
6//! - Retrieving captured requests
7//! - Triggering contract diff analysis
8
9use 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
25/// Helper to convert Error to HTTP response
26fn 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
37/// Upload a request manually for contract diff analysis
38pub 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    // Capture the request
56    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
91/// Submit a request programmatically via API
92pub 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    // Capture the request
110    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
138/// Get captured requests with optional filters
139pub 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
188/// Get a specific captured request by ID
189pub 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
244/// Analyze a captured request against a contract specification
245pub 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    // Get the captured request
264    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    // Load the contract specification
279    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        // Try to detect format from content
283        let format = if spec_content.trim_start().starts_with('{') {
284            None // JSON
285        } else {
286            Some("yaml") // YAML
287        };
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    // Create contract diff analyzer
304    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    // Analyze the request
311    let result = match analyzer.analyze(&request, &spec).await {
312        Ok(result) => result,
313        Err(e) => return error_response(e),
314    };
315
316    // Mark as analyzed
317    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
337/// Get capture statistics
338pub 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
365/// Generate patch file for correction proposals
366pub 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    // Get the captured request
385    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    // Load the contract specification
400    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 // JSON
405        } else {
406            Some("yaml") // YAML
407        };
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    // Create contract diff analyzer
424    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    // Analyze the request
431    let result = match analyzer.analyze(&request, &spec).await {
432        Ok(result) => result,
433        Err(e) => return error_response(e),
434    };
435
436    // Generate patch file
437    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/// Request payload for patch generation
467#[derive(Debug, Deserialize)]
468pub struct GeneratePatchPayload {
469    /// Path to contract specification file
470    pub spec_path: Option<String>,
471
472    /// Contract specification content (OpenAPI YAML/JSON)
473    pub spec_content: Option<String>,
474
475    /// Contract diff configuration
476    pub config: Option<ContractDiffConfig>,
477}
478
479/// Request payload for manual upload
480#[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/// Request payload for programmatic submission
492#[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/// Request payload for analysis
504#[derive(Debug, Deserialize)]
505pub struct AnalyzeRequestPayload {
506    /// Path to contract specification file
507    pub spec_path: Option<String>,
508
509    /// Contract specification content (OpenAPI YAML/JSON)
510    pub spec_content: Option<String>,
511
512    /// Contract ID for tracking
513    pub contract_id: Option<String>,
514
515    /// Contract diff configuration
516    pub config: Option<ContractDiffConfig>,
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    // ==================== UploadRequestPayload Tests ====================
524
525    #[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    // ==================== SubmitRequestPayload Tests ====================
597
598    #[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    // ==================== AnalyzeRequestPayload Tests ====================
644
645    #[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    // ==================== GeneratePatchPayload Tests ====================
703
704    #[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    // ==================== Helper Function Tests ====================
739
740    #[test]
741    fn test_error_response_creation() {
742        let error = Error::validation("test error");
743        let response = error_response(error);
744        // Response is created successfully
745        let _ = response;
746    }
747
748    // ==================== HTTP Method Coverage ====================
749
750    #[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    // ==================== Edge Cases ====================
789
790    #[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}