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