mockforge_http/
contract_diff_middleware.rs1use 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
16pub 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 let headers = extract_headers_for_capture(req.headers());
25
26 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 let query_params = if let Some(query) = query {
35 parse_query_params(query)
36 } else {
37 HashMap::new()
38 };
39
40 let response = next.run(req).await;
46
47 let status_code = response.status().as_u16();
49
50 let captured = CapturedRequest::new(&method, &path, "proxy_middleware")
52 .with_headers(headers)
53 .with_query_params(query_params)
54 .with_response(status_code, None); if let Some(ua) = user_agent {
57 let mut metadata = HashMap::new();
60 metadata.insert("user_agent".to_string(), Value::String(ua));
61 }
63
64 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
74fn extract_headers_for_capture(headers: &HeaderMap) -> HashMap<String, String> {
76 let mut captured_headers = HashMap::new();
77
78 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
102fn 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")); }
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}