mockforge_recorder/
models.rs

1//! Data models for recorded requests and responses
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Protocol type
8#[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/// Tracing context for requests (OpenTelemetry)
40#[derive(Debug, Clone, Default)]
41pub struct RequestContext {
42    /// Client IP address
43    pub client_ip: Option<String>,
44    /// Trace ID (from OpenTelemetry)
45    pub trace_id: Option<String>,
46    /// Span ID (from OpenTelemetry)
47    pub span_id: Option<String>,
48}
49
50impl RequestContext {
51    /// Create a new request context
52    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/// Recorded HTTP/API request
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RecordedRequest {
64    /// Unique request ID
65    pub id: String,
66    /// Protocol type
67    pub protocol: Protocol,
68    /// Timestamp
69    pub timestamp: DateTime<Utc>,
70    /// HTTP method or gRPC method name
71    pub method: String,
72    /// Request path or endpoint
73    pub path: String,
74    /// Query parameters (for HTTP)
75    pub query_params: Option<String>,
76    /// Request headers (JSON)
77    pub headers: String,
78    /// Request body (may be base64 encoded for binary)
79    pub body: Option<String>,
80    /// Body encoding (utf8, base64)
81    pub body_encoding: String,
82    /// Client IP address
83    pub client_ip: Option<String>,
84    /// Trace ID (from OpenTelemetry)
85    pub trace_id: Option<String>,
86    /// Span ID (from OpenTelemetry)
87    pub span_id: Option<String>,
88    /// Duration in milliseconds
89    pub duration_ms: Option<i64>,
90    /// Response status code
91    pub status_code: Option<i32>,
92    /// Tags for categorization (JSON array)
93    pub tags: Option<String>,
94}
95
96impl RecordedRequest {
97    /// Parse headers from JSON string
98    pub fn headers_map(&self) -> HashMap<String, String> {
99        serde_json::from_str(&self.headers).unwrap_or_default()
100    }
101
102    /// Parse query parameters
103    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    /// Parse tags
111    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    /// Decode body based on encoding
119    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/// Recorded response
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RecordedResponse {
134    /// Response ID (same as request ID)
135    pub request_id: String,
136    /// Response status code
137    pub status_code: i32,
138    /// Response headers (JSON)
139    pub headers: String,
140    /// Response body (may be base64 encoded for binary)
141    pub body: Option<String>,
142    /// Body encoding (utf8, base64)
143    pub body_encoding: String,
144    /// Response size in bytes
145    pub size_bytes: i64,
146    /// Timestamp
147    pub timestamp: DateTime<Utc>,
148}
149
150impl RecordedResponse {
151    /// Parse headers from JSON string
152    pub fn headers_map(&self) -> HashMap<String, String> {
153        serde_json::from_str(&self.headers).unwrap_or_default()
154    }
155
156    /// Decode body based on encoding
157    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/// Request/Response pair
170#[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    // ==================== Protocol Tests ====================
181
182    #[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    // ==================== RequestContext Tests ====================
226
227    #[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    // ==================== RecordedRequest Tests ====================
261
262    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()); // "hello world" in base64
372        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    // ==================== RecordedResponse Tests ====================
405
406    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()); // "test body" in base64
445        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    // ==================== RecordedExchange Tests ====================
476
477    #[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}