mockforge_http/
op_middleware.rs1use axum::body::Body;
3use axum::extract::State;
4use axum::http::{Request, StatusCode};
5use axum::middleware::Next;
6use axum::response::Response;
7
8use crate::latency_profiles::LatencyProfiles;
9use mockforge_chaos::core_failure_injection::FailureInjector;
10use mockforge_chaos::core_traffic_shaping::TrafficShaper;
11
12#[derive(Clone)]
14pub struct OperationMeta {
15 pub id: String,
17 pub tags: Vec<String>,
19 pub path: String,
21}
22
23#[derive(Clone)]
25pub struct Shared {
26 pub profiles: LatencyProfiles,
28 pub failure_injector: Option<FailureInjector>,
30 pub traffic_shaper: Option<TrafficShaper>,
32 pub traffic_shaping_enabled: bool,
34}
35
36pub async fn add_shared_extension(
38 State(shared): State<Shared>,
39 mut req: Request<Body>,
40 next: Next,
41) -> Response {
42 req.extensions_mut().insert(shared);
43 next.run(req).await
44}
45
46pub async fn fault_then_next(req: Request<Body>, next: Next) -> Response {
48 let shared = match req.extensions().get::<Shared>() {
49 Some(s) => s.clone(),
50 None => {
51 tracing::error!("Shared extension not found in request - ensure add_shared_extension middleware is configured");
52 let mut res =
53 Response::new(Body::from("Internal server error: middleware misconfiguration"));
54 *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
55 return res;
56 }
57 };
58 let op = req.extensions().get::<OperationMeta>().cloned();
59
60 if let Some(failure_injector) = &shared.failure_injector {
62 let tags = op.as_ref().map(|o| o.tags.as_slice()).unwrap_or(&[]);
63 if let Some((status_code, error_message)) = failure_injector.process_request(tags) {
64 let mut res = Response::new(Body::from(error_message));
65 *res.status_mut() =
66 StatusCode::from_u16(status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
67 return res;
68 }
69 }
70
71 if let Some(op) = &op {
73 if let Some((code, msg)) = shared
74 .profiles
75 .maybe_fault(&op.id, &op.tags.iter().map(|s| s.to_string()).collect::<Vec<_>>())
76 .await
77 {
78 let mut res = Response::new(Body::from(msg));
79 *res.status_mut() =
80 StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
81 return res;
82 }
83 }
84
85 if shared.traffic_shaping_enabled {
87 if let Some(traffic_shaper) = &shared.traffic_shaper {
88 let request_size = calculate_request_size(&req);
90
91 let tags = op.as_ref().map(|o| o.tags.as_slice()).unwrap_or(&[]);
92
93 match traffic_shaper.process_transfer(request_size, tags).await {
95 Ok(Some(_timeout)) => {
96 let mut res =
98 Response::new(Body::from("Request timeout due to traffic shaping"));
99 *res.status_mut() = StatusCode::REQUEST_TIMEOUT;
100 return res;
101 }
102 Ok(None) => {
103 }
105 Err(e) => {
106 let mut res =
108 Response::new(Body::from(format!("Traffic shaping error: {}", e)));
109 *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
110 return res;
111 }
112 }
113 }
114 }
115
116 let (parts, body) = req.into_parts();
117 let req = Request::from_parts(parts, body);
118
119 let response = next.run(req).await;
120
121 if shared.traffic_shaping_enabled {
123 if let Some(traffic_shaper) = &shared.traffic_shaper {
124 let response_size = calculate_response_size(&response);
126
127 let tags = op.as_ref().map(|o| o.tags.as_slice()).unwrap_or(&[]);
128
129 match traffic_shaper.process_transfer(response_size, tags).await {
131 Ok(Some(_timeout)) => {
132 let mut res =
134 Response::new(Body::from("Response timeout due to traffic shaping"));
135 *res.status_mut() = StatusCode::GATEWAY_TIMEOUT;
136 return res;
137 }
138 Ok(None) => {
139 }
141 Err(e) => {
142 let mut res =
144 Response::new(Body::from(format!("Traffic shaping error: {}", e)));
145 *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
146 return res;
147 }
148 }
149 }
150 }
151
152 response
153}
154
155fn calculate_request_size<B>(req: &Request<B>) -> u64 {
157 let mut size = 0u64;
158
159 for (name, value) in req.headers() {
161 size += name.as_str().len() as u64;
162 size += value.as_bytes().len() as u64;
163 }
164
165 size += req.uri().to_string().len() as u64;
167
168 if let Some(content_length) = req.headers().get(http::header::CONTENT_LENGTH) {
170 if let Ok(len_str) = content_length.to_str() {
171 if let Ok(len) = len_str.parse::<u64>() {
172 size += len;
173 return size;
174 }
175 }
176 }
177
178 let method = req.method();
180 if method == http::Method::POST || method == http::Method::PUT || method == http::Method::PATCH
181 {
182 size += 256; }
184
185 size
186}
187
188fn calculate_response_size(res: &Response) -> u64 {
190 let mut size = 0u64;
191
192 for (name, value) in res.headers() {
194 size += name.as_str().len() as u64;
195 size += value.as_bytes().len() as u64;
196 }
197
198 size += 15; if let Some(content_length) = res.headers().get(http::header::CONTENT_LENGTH) {
203 if let Ok(len_str) = content_length.to_str() {
204 if let Ok(len) = len_str.parse::<u64>() {
205 size += len;
206 return size;
207 }
208 }
209 }
210
211 match res.status().as_u16() {
213 204 | 304 => {} _ => size += 256, }
216
217 size
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use axum::http::{Request, Response, StatusCode};
224
225 #[test]
226 fn test_operation_meta_creation() {
227 let meta = OperationMeta {
228 id: "getUserById".to_string(),
229 tags: vec!["users".to_string(), "public".to_string()],
230 path: "/users/{id}".to_string(),
231 };
232
233 assert_eq!(meta.id, "getUserById");
234 assert_eq!(meta.tags.len(), 2);
235 assert_eq!(meta.path, "/users/{id}");
236 }
237
238 #[test]
239 fn test_shared_creation() {
240 let shared = Shared {
241 profiles: LatencyProfiles::default(),
242 failure_injector: None,
243 traffic_shaper: None,
244 traffic_shaping_enabled: false,
245 };
246
247 assert!(!shared.traffic_shaping_enabled);
248 assert!(shared.failure_injector.is_none());
249 assert!(shared.traffic_shaper.is_none());
250 }
251
252 #[test]
253 fn test_shared_with_failure_injector() {
254 let failure_injector = FailureInjector::new(None, true);
255 let shared = Shared {
256 profiles: LatencyProfiles::default(),
257 failure_injector: Some(failure_injector),
258 traffic_shaper: None,
259 traffic_shaping_enabled: false,
260 };
261
262 assert!(shared.failure_injector.is_some());
263 }
264
265 #[test]
266 fn test_calculate_request_size_basic() {
267 let req = Request::builder()
268 .uri("/test")
269 .header("content-type", "application/json")
270 .body(())
271 .unwrap();
272
273 let size = calculate_request_size(&req);
274
275 assert!(size > 0);
277 assert!(size >= "/test".len() as u64 + "content-type".len() as u64);
279 }
280
281 #[test]
282 fn test_calculate_request_size_with_multiple_headers() {
283 let req = Request::builder()
284 .uri("/api/users")
285 .header("content-type", "application/json")
286 .header("authorization", "Bearer token123")
287 .header("user-agent", "test-client")
288 .body(())
289 .unwrap();
290
291 let size = calculate_request_size(&req);
292
293 assert!(size > 50);
295 }
296
297 #[test]
298 fn test_calculate_response_size_basic() {
299 let res = Response::builder()
300 .status(StatusCode::OK)
301 .header("content-type", "application/json")
302 .body(Body::empty())
303 .unwrap();
304
305 let size = calculate_response_size(&res);
306
307 assert!(size > 0);
309 assert!(size >= 50);
311 }
312
313 #[test]
314 fn test_calculate_response_size_with_multiple_headers() {
315 let res = Response::builder()
316 .status(StatusCode::OK)
317 .header("content-type", "application/json")
318 .header("cache-control", "no-cache")
319 .header("x-request-id", "123-456-789")
320 .body(Body::empty())
321 .unwrap();
322
323 let size = calculate_response_size(&res);
324
325 assert!(size > 100);
327 }
328
329 #[test]
330 fn test_shared_clone() {
331 let shared = Shared {
332 profiles: LatencyProfiles::default(),
333 failure_injector: None,
334 traffic_shaper: None,
335 traffic_shaping_enabled: true,
336 };
337
338 let cloned = shared.clone();
339
340 assert_eq!(shared.traffic_shaping_enabled, cloned.traffic_shaping_enabled);
341 }
342
343 #[test]
344 fn test_operation_meta_clone() {
345 let meta = OperationMeta {
346 id: "testOp".to_string(),
347 tags: vec!["tag1".to_string()],
348 path: "/test".to_string(),
349 };
350
351 let cloned = meta.clone();
352
353 assert_eq!(meta.id, cloned.id);
354 assert_eq!(meta.tags, cloned.tags);
355 assert_eq!(meta.path, cloned.path);
356 }
357}