mockforge_http/
contract_diff_middleware.rs1use 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
19pub 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 let headers = extract_headers_for_capture(req.headers());
28
29 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 let query_params = if let Some(query) = query {
38 parse_query_params(query)
39 } else {
40 HashMap::new()
41 };
42
43 let response = next.run(req).await;
49
50 let status_code = response.status().as_u16();
52
53 let captured = CapturedRequest::new(&method, &path, "proxy_middleware")
55 .with_headers(headers)
56 .with_query_params(query_params)
57 .with_response(status_code, None); if let Some(ua) = user_agent {
60 let mut metadata = HashMap::new();
63 metadata.insert("user_agent".to_string(), Value::String(ua));
64 }
66
67 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
77fn extract_headers_for_capture(headers: &HeaderMap) -> HashMap<String, String> {
79 let mut captured_headers = HashMap::new();
80
81 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
105fn 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")); }
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}