mockforge_http/
contract_diff_middleware.rs

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