mockforge_http/
contract_diff_middleware.rs

1//! Pillars: [Contracts]
2//!
3//! Contract diff middleware for capturing requests
4//!
5//! This middleware captures incoming HTTP requests for contract diff analysis.
6//! It extracts request data and stores it in the capture manager.
7
8use axum::http::HeaderMap;
9use axum::{body::Body, extract::Request, middleware::Next, response::Response};
10use mockforge_core::{
11    ai_contract_diff::CapturedRequest, request_capture::get_global_capture_manager,
12};
13use serde_json::Value;
14use std::collections::HashMap;
15use tracing::debug;
16
17/// Middleware to capture requests for contract diff analysis
18pub async fn capture_for_contract_diff(req: Request<Body>, next: Next) -> Response {
19    let method = req.method().to_string();
20    let uri = req.uri().clone();
21    let path = uri.path().to_string();
22    let query = uri.query();
23
24    // Extract headers
25    let headers = extract_headers_for_capture(req.headers());
26
27    // Extract user agent
28    let user_agent = req
29        .headers()
30        .get("user-agent")
31        .and_then(|h| h.to_str().ok())
32        .map(|s| s.to_string());
33
34    // Extract query parameters
35    let query_params = if let Some(query) = query {
36        parse_query_params(query)
37    } else {
38        HashMap::new()
39    };
40
41    // Clone request body for capture (we'll read it after the response)
42    // Note: In a real implementation, we'd need to buffer the body
43    // For now, we'll capture what we can without the body
44
45    // Call the next middleware/handler
46    let response = next.run(req).await;
47
48    // Extract response status
49    let status_code = response.status().as_u16();
50
51    // Create captured request
52    let captured = CapturedRequest::new(&method, &path, "proxy_middleware")
53        .with_headers(headers)
54        .with_query_params(query_params)
55        .with_response(status_code, None); // Response body capture would require buffering
56
57    if let Some(ua) = user_agent {
58        // Note: CapturedRequest doesn't have a with_user_agent method yet
59        // We'll add it to metadata for now
60        let mut metadata = HashMap::new();
61        metadata.insert("user_agent".to_string(), Value::String(ua));
62        // We can't modify captured here, but we could extend CapturedRequest
63    }
64
65    // Capture the request (fire and forget)
66    if let Some(capture_manager) = get_global_capture_manager() {
67        if let Err(e) = capture_manager.capture(captured).await {
68            debug!("Failed to capture request for contract diff: {}", e);
69        }
70    }
71
72    response
73}
74
75/// Extract headers for capture (excluding sensitive ones)
76fn extract_headers_for_capture(headers: &HeaderMap) -> HashMap<String, String> {
77    let mut captured_headers = HashMap::new();
78
79    // Safe headers to capture
80    let safe_headers = [
81        "accept",
82        "accept-encoding",
83        "accept-language",
84        "content-type",
85        "content-length",
86        "user-agent",
87        "referer",
88        "origin",
89        "x-requested-with",
90    ];
91
92    for header_name in safe_headers {
93        if let Some(value) = headers.get(header_name) {
94            if let Ok(value_str) = value.to_str() {
95                captured_headers.insert(header_name.to_string(), value_str.to_string());
96            }
97        }
98    }
99
100    captured_headers
101}
102
103/// Parse query string into HashMap
104fn parse_query_params(query: &str) -> HashMap<String, String> {
105    let mut params = HashMap::new();
106
107    for pair in query.split('&') {
108        if let Some((key, value)) = pair.split_once('=') {
109            let decoded_key = urlencoding::decode(key).unwrap_or_else(|_| key.into());
110            let decoded_value = urlencoding::decode(value).unwrap_or_else(|_| value.into());
111            params.insert(decoded_key.to_string(), decoded_value.to_string());
112        }
113    }
114
115    params
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use axum::http::HeaderValue;
122
123    #[test]
124    fn test_extract_headers_for_capture() {
125        let mut headers = HeaderMap::new();
126        headers.insert("content-type", HeaderValue::from_static("application/json"));
127        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
128        headers.insert("accept", HeaderValue::from_static("application/json"));
129
130        let captured = extract_headers_for_capture(&headers);
131
132        assert_eq!(captured.get("content-type"), Some(&"application/json".to_string()));
133        assert_eq!(captured.get("accept"), Some(&"application/json".to_string()));
134        assert!(!captured.contains_key("authorization")); // Should exclude sensitive headers
135    }
136
137    #[test]
138    fn test_parse_query_params() {
139        let query = "name=John&age=30&city=New%20York";
140        let params = parse_query_params(query);
141
142        assert_eq!(params.get("name"), Some(&"John".to_string()));
143        assert_eq!(params.get("age"), Some(&"30".to_string()));
144        assert_eq!(params.get("city"), Some(&"New York".to_string()));
145    }
146
147    #[test]
148    fn test_parse_query_params_empty() {
149        let params = parse_query_params("");
150        assert!(params.is_empty());
151    }
152}