1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
9#[sqlx(type_name = "TEXT")]
10#[sqlx(rename_all = "lowercase")]
11pub enum Protocol {
12 #[sqlx(rename = "http")]
13 Http,
14 #[sqlx(rename = "grpc")]
15 Grpc,
16 #[sqlx(rename = "websocket")]
17 WebSocket,
18 #[sqlx(rename = "graphql")]
19 GraphQL,
20}
21
22impl Protocol {
23 pub fn as_str(&self) -> &'static str {
24 match self {
25 Protocol::Http => "http",
26 Protocol::Grpc => "grpc",
27 Protocol::WebSocket => "websocket",
28 Protocol::GraphQL => "graphql",
29 }
30 }
31}
32
33impl std::fmt::Display for Protocol {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "{}", self.as_str())
36 }
37}
38
39#[derive(Debug, Clone, Default)]
41pub struct RequestContext {
42 pub client_ip: Option<String>,
44 pub trace_id: Option<String>,
46 pub span_id: Option<String>,
48}
49
50impl RequestContext {
51 pub fn new(client_ip: Option<&str>, trace_id: Option<&str>, span_id: Option<&str>) -> Self {
53 Self {
54 client_ip: client_ip.map(|s| s.to_string()),
55 trace_id: trace_id.map(|s| s.to_string()),
56 span_id: span_id.map(|s| s.to_string()),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RecordedRequest {
64 pub id: String,
66 pub protocol: Protocol,
68 pub timestamp: DateTime<Utc>,
70 pub method: String,
72 pub path: String,
74 pub query_params: Option<String>,
76 pub headers: String,
78 pub body: Option<String>,
80 pub body_encoding: String,
82 pub client_ip: Option<String>,
84 pub trace_id: Option<String>,
86 pub span_id: Option<String>,
88 pub duration_ms: Option<i64>,
90 pub status_code: Option<i32>,
92 pub tags: Option<String>,
94}
95
96impl RecordedRequest {
97 pub fn headers_map(&self) -> HashMap<String, String> {
99 serde_json::from_str(&self.headers).unwrap_or_default()
100 }
101
102 pub fn query_params_map(&self) -> HashMap<String, String> {
104 self.query_params
105 .as_ref()
106 .and_then(|q| serde_json::from_str(q).ok())
107 .unwrap_or_default()
108 }
109
110 pub fn tags_vec(&self) -> Vec<String> {
112 self.tags
113 .as_ref()
114 .and_then(|t| serde_json::from_str(t).ok())
115 .unwrap_or_default()
116 }
117
118 pub fn decoded_body(&self) -> Option<Vec<u8>> {
120 self.body.as_ref().map(|body| {
121 if self.body_encoding == "base64" {
122 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, body)
123 .unwrap_or_else(|_| body.as_bytes().to_vec())
124 } else {
125 body.as_bytes().to_vec()
126 }
127 })
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RecordedResponse {
134 pub request_id: String,
136 pub status_code: i32,
138 pub headers: String,
140 pub body: Option<String>,
142 pub body_encoding: String,
144 pub size_bytes: i64,
146 pub timestamp: DateTime<Utc>,
148}
149
150impl RecordedResponse {
151 pub fn headers_map(&self) -> HashMap<String, String> {
153 serde_json::from_str(&self.headers).unwrap_or_default()
154 }
155
156 pub fn decoded_body(&self) -> Option<Vec<u8>> {
158 self.body.as_ref().map(|body| {
159 if self.body_encoding == "base64" {
160 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, body)
161 .unwrap_or_else(|_| body.as_bytes().to_vec())
162 } else {
163 body.as_bytes().to_vec()
164 }
165 })
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct RecordedExchange {
172 pub request: RecordedRequest,
173 pub response: Option<RecordedResponse>,
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
183 fn test_protocol_display() {
184 assert_eq!(Protocol::Http.to_string(), "http");
185 assert_eq!(Protocol::Grpc.to_string(), "grpc");
186 assert_eq!(Protocol::WebSocket.to_string(), "websocket");
187 assert_eq!(Protocol::GraphQL.to_string(), "graphql");
188 }
189
190 #[test]
191 fn test_protocol_as_str() {
192 assert_eq!(Protocol::Http.as_str(), "http");
193 assert_eq!(Protocol::Grpc.as_str(), "grpc");
194 assert_eq!(Protocol::WebSocket.as_str(), "websocket");
195 assert_eq!(Protocol::GraphQL.as_str(), "graphql");
196 }
197
198 #[test]
199 fn test_protocol_equality() {
200 assert_eq!(Protocol::Http, Protocol::Http);
201 assert_ne!(Protocol::Http, Protocol::Grpc);
202 }
203
204 #[test]
205 fn test_protocol_clone() {
206 let proto = Protocol::Http;
207 let cloned = proto.clone();
208 assert_eq!(proto, cloned);
209 }
210
211 #[test]
212 fn test_protocol_serialize() {
213 let proto = Protocol::Http;
214 let json = serde_json::to_string(&proto).unwrap();
215 assert_eq!(json, "\"Http\"");
216 }
217
218 #[test]
219 fn test_protocol_deserialize() {
220 let json = "\"Grpc\"";
221 let proto: Protocol = serde_json::from_str(json).unwrap();
222 assert_eq!(proto, Protocol::Grpc);
223 }
224
225 #[test]
228 fn test_request_context_new() {
229 let ctx = RequestContext::new(Some("192.168.1.1"), Some("trace-123"), Some("span-456"));
230 assert_eq!(ctx.client_ip, Some("192.168.1.1".to_string()));
231 assert_eq!(ctx.trace_id, Some("trace-123".to_string()));
232 assert_eq!(ctx.span_id, Some("span-456".to_string()));
233 }
234
235 #[test]
236 fn test_request_context_new_with_nones() {
237 let ctx = RequestContext::new(None, None, None);
238 assert!(ctx.client_ip.is_none());
239 assert!(ctx.trace_id.is_none());
240 assert!(ctx.span_id.is_none());
241 }
242
243 #[test]
244 fn test_request_context_default() {
245 let ctx = RequestContext::default();
246 assert!(ctx.client_ip.is_none());
247 assert!(ctx.trace_id.is_none());
248 assert!(ctx.span_id.is_none());
249 }
250
251 #[test]
252 fn test_request_context_clone() {
253 let ctx = RequestContext::new(Some("127.0.0.1"), Some("trace"), Some("span"));
254 let cloned = ctx.clone();
255 assert_eq!(ctx.client_ip, cloned.client_ip);
256 assert_eq!(ctx.trace_id, cloned.trace_id);
257 assert_eq!(ctx.span_id, cloned.span_id);
258 }
259
260 fn create_test_request() -> RecordedRequest {
263 RecordedRequest {
264 id: "test-123".to_string(),
265 protocol: Protocol::Http,
266 timestamp: Utc::now(),
267 method: "GET".to_string(),
268 path: "/api/users".to_string(),
269 query_params: Some(r#"{"page":"1","limit":"10"}"#.to_string()),
270 headers: r#"{"content-type":"application/json","authorization":"Bearer token"}"#
271 .to_string(),
272 body: Some("hello world".to_string()),
273 body_encoding: "utf8".to_string(),
274 client_ip: Some("192.168.1.1".to_string()),
275 trace_id: Some("trace-abc".to_string()),
276 span_id: Some("span-xyz".to_string()),
277 duration_ms: Some(150),
278 status_code: Some(200),
279 tags: Some(r#"["api","users","test"]"#.to_string()),
280 }
281 }
282
283 #[test]
284 fn test_headers_parsing() {
285 let request = RecordedRequest {
286 id: "test".to_string(),
287 protocol: Protocol::Http,
288 timestamp: Utc::now(),
289 method: "GET".to_string(),
290 path: "/test".to_string(),
291 query_params: None,
292 headers: r#"{"content-type":"application/json"}"#.to_string(),
293 body: None,
294 body_encoding: "utf8".to_string(),
295 client_ip: None,
296 trace_id: None,
297 span_id: None,
298 duration_ms: None,
299 status_code: None,
300 tags: Some(r#"["test","api"]"#.to_string()),
301 };
302
303 let headers = request.headers_map();
304 assert_eq!(headers.get("content-type"), Some(&"application/json".to_string()));
305
306 let tags = request.tags_vec();
307 assert_eq!(tags, vec!["test", "api"]);
308 }
309
310 #[test]
311 fn test_recorded_request_headers_map() {
312 let request = create_test_request();
313 let headers = request.headers_map();
314 assert_eq!(headers.get("content-type"), Some(&"application/json".to_string()));
315 assert_eq!(headers.get("authorization"), Some(&"Bearer token".to_string()));
316 }
317
318 #[test]
319 fn test_recorded_request_headers_map_invalid_json() {
320 let mut request = create_test_request();
321 request.headers = "invalid json".to_string();
322 let headers = request.headers_map();
323 assert!(headers.is_empty());
324 }
325
326 #[test]
327 fn test_recorded_request_query_params_map() {
328 let request = create_test_request();
329 let params = request.query_params_map();
330 assert_eq!(params.get("page"), Some(&"1".to_string()));
331 assert_eq!(params.get("limit"), Some(&"10".to_string()));
332 }
333
334 #[test]
335 fn test_recorded_request_query_params_map_none() {
336 let mut request = create_test_request();
337 request.query_params = None;
338 let params = request.query_params_map();
339 assert!(params.is_empty());
340 }
341
342 #[test]
343 fn test_recorded_request_tags_vec() {
344 let request = create_test_request();
345 let tags = request.tags_vec();
346 assert_eq!(tags.len(), 3);
347 assert!(tags.contains(&"api".to_string()));
348 assert!(tags.contains(&"users".to_string()));
349 assert!(tags.contains(&"test".to_string()));
350 }
351
352 #[test]
353 fn test_recorded_request_tags_vec_none() {
354 let mut request = create_test_request();
355 request.tags = None;
356 let tags = request.tags_vec();
357 assert!(tags.is_empty());
358 }
359
360 #[test]
361 fn test_recorded_request_decoded_body_utf8() {
362 let request = create_test_request();
363 let body = request.decoded_body();
364 assert!(body.is_some());
365 assert_eq!(body.unwrap(), b"hello world".to_vec());
366 }
367
368 #[test]
369 fn test_recorded_request_decoded_body_base64() {
370 let mut request = create_test_request();
371 request.body = Some("aGVsbG8gd29ybGQ=".to_string()); request.body_encoding = "base64".to_string();
373 let body = request.decoded_body();
374 assert!(body.is_some());
375 assert_eq!(body.unwrap(), b"hello world".to_vec());
376 }
377
378 #[test]
379 fn test_recorded_request_decoded_body_none() {
380 let mut request = create_test_request();
381 request.body = None;
382 let body = request.decoded_body();
383 assert!(body.is_none());
384 }
385
386 #[test]
387 fn test_recorded_request_serialize() {
388 let request = create_test_request();
389 let json = serde_json::to_string(&request).unwrap();
390 assert!(json.contains("test-123"));
391 assert!(json.contains("GET"));
392 assert!(json.contains("/api/users"));
393 }
394
395 #[test]
396 fn test_recorded_request_clone() {
397 let request = create_test_request();
398 let cloned = request.clone();
399 assert_eq!(request.id, cloned.id);
400 assert_eq!(request.method, cloned.method);
401 assert_eq!(request.path, cloned.path);
402 }
403
404 fn create_test_response() -> RecordedResponse {
407 RecordedResponse {
408 request_id: "test-123".to_string(),
409 status_code: 200,
410 headers: r#"{"content-type":"application/json"}"#.to_string(),
411 body: Some(r#"{"status":"ok"}"#.to_string()),
412 body_encoding: "utf8".to_string(),
413 size_bytes: 15,
414 timestamp: Utc::now(),
415 }
416 }
417
418 #[test]
419 fn test_recorded_response_headers_map() {
420 let response = create_test_response();
421 let headers = response.headers_map();
422 assert_eq!(headers.get("content-type"), Some(&"application/json".to_string()));
423 }
424
425 #[test]
426 fn test_recorded_response_headers_map_invalid_json() {
427 let mut response = create_test_response();
428 response.headers = "invalid".to_string();
429 let headers = response.headers_map();
430 assert!(headers.is_empty());
431 }
432
433 #[test]
434 fn test_recorded_response_decoded_body_utf8() {
435 let response = create_test_response();
436 let body = response.decoded_body();
437 assert!(body.is_some());
438 assert_eq!(body.unwrap(), br#"{"status":"ok"}"#.to_vec());
439 }
440
441 #[test]
442 fn test_recorded_response_decoded_body_base64() {
443 let mut response = create_test_response();
444 response.body = Some("dGVzdCBib2R5".to_string()); response.body_encoding = "base64".to_string();
446 let body = response.decoded_body();
447 assert!(body.is_some());
448 assert_eq!(body.unwrap(), b"test body".to_vec());
449 }
450
451 #[test]
452 fn test_recorded_response_decoded_body_none() {
453 let mut response = create_test_response();
454 response.body = None;
455 let body = response.decoded_body();
456 assert!(body.is_none());
457 }
458
459 #[test]
460 fn test_recorded_response_clone() {
461 let response = create_test_response();
462 let cloned = response.clone();
463 assert_eq!(response.request_id, cloned.request_id);
464 assert_eq!(response.status_code, cloned.status_code);
465 }
466
467 #[test]
468 fn test_recorded_response_serialize() {
469 let response = create_test_response();
470 let json = serde_json::to_string(&response).unwrap();
471 assert!(json.contains("test-123"));
472 assert!(json.contains("200"));
473 }
474
475 #[test]
478 fn test_recorded_exchange_with_response() {
479 let exchange = RecordedExchange {
480 request: create_test_request(),
481 response: Some(create_test_response()),
482 };
483 assert!(exchange.response.is_some());
484 assert_eq!(exchange.request.id, "test-123");
485 }
486
487 #[test]
488 fn test_recorded_exchange_without_response() {
489 let exchange = RecordedExchange {
490 request: create_test_request(),
491 response: None,
492 };
493 assert!(exchange.response.is_none());
494 }
495
496 #[test]
497 fn test_recorded_exchange_serialize() {
498 let exchange = RecordedExchange {
499 request: create_test_request(),
500 response: Some(create_test_response()),
501 };
502 let json = serde_json::to_string(&exchange).unwrap();
503 assert!(json.contains("request"));
504 assert!(json.contains("response"));
505 }
506
507 #[test]
508 fn test_recorded_exchange_clone() {
509 let exchange = RecordedExchange {
510 request: create_test_request(),
511 response: Some(create_test_response()),
512 };
513 let cloned = exchange.clone();
514 assert_eq!(exchange.request.id, cloned.request.id);
515 assert!(cloned.response.is_some());
516 }
517}