mockforge_analytics/
models.rs

1//! Data models for analytics
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Granularity level for aggregated metrics
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum Granularity {
11    Minute,
12    Hour,
13    Day,
14}
15
16/// Aggregated metrics for a specific time window
17#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
18pub struct MetricsAggregate {
19    pub id: Option<i64>,
20    pub timestamp: i64,
21    pub protocol: String,
22    pub method: Option<String>,
23    pub endpoint: Option<String>,
24    pub status_code: Option<i32>,
25    pub workspace_id: Option<String>,
26    pub environment: Option<String>,
27    pub request_count: i64,
28    pub error_count: i64,
29    pub latency_sum: f64,
30    pub latency_min: Option<f64>,
31    pub latency_max: Option<f64>,
32    pub latency_p50: Option<f64>,
33    pub latency_p95: Option<f64>,
34    pub latency_p99: Option<f64>,
35    pub bytes_sent: i64,
36    pub bytes_received: i64,
37    pub active_connections: Option<i64>,
38    pub created_at: Option<i64>,
39}
40
41/// Hour-level aggregated metrics
42#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
43pub struct HourMetricsAggregate {
44    pub id: Option<i64>,
45    pub timestamp: i64,
46    pub protocol: String,
47    pub method: Option<String>,
48    pub endpoint: Option<String>,
49    pub status_code: Option<i32>,
50    pub workspace_id: Option<String>,
51    pub environment: Option<String>,
52    pub request_count: i64,
53    pub error_count: i64,
54    pub latency_sum: f64,
55    pub latency_min: Option<f64>,
56    pub latency_max: Option<f64>,
57    pub latency_p50: Option<f64>,
58    pub latency_p95: Option<f64>,
59    pub latency_p99: Option<f64>,
60    pub bytes_sent: i64,
61    pub bytes_received: i64,
62    pub active_connections_avg: Option<f64>,
63    pub active_connections_max: Option<i64>,
64    pub created_at: Option<i64>,
65}
66
67/// Day-level aggregated metrics
68#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
69pub struct DayMetricsAggregate {
70    pub id: Option<i64>,
71    pub date: String,
72    pub timestamp: i64,
73    pub protocol: String,
74    pub method: Option<String>,
75    pub endpoint: Option<String>,
76    pub status_code: Option<i32>,
77    pub workspace_id: Option<String>,
78    pub environment: Option<String>,
79    pub request_count: i64,
80    pub error_count: i64,
81    pub latency_sum: f64,
82    pub latency_min: Option<f64>,
83    pub latency_max: Option<f64>,
84    pub latency_p50: Option<f64>,
85    pub latency_p95: Option<f64>,
86    pub latency_p99: Option<f64>,
87    pub bytes_sent: i64,
88    pub bytes_received: i64,
89    pub active_connections_avg: Option<f64>,
90    pub active_connections_max: Option<i64>,
91    pub unique_clients: Option<i64>,
92    pub peak_hour: Option<i32>,
93    pub created_at: Option<i64>,
94}
95
96/// Statistics for a specific endpoint
97#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
98pub struct EndpointStats {
99    pub id: Option<i64>,
100    pub endpoint: String,
101    pub protocol: String,
102    pub method: Option<String>,
103    pub workspace_id: Option<String>,
104    pub environment: Option<String>,
105    pub total_requests: i64,
106    pub total_errors: i64,
107    pub avg_latency_ms: Option<f64>,
108    pub min_latency_ms: Option<f64>,
109    pub max_latency_ms: Option<f64>,
110    pub p95_latency_ms: Option<f64>,
111    pub status_codes: Option<String>, // JSON
112    pub total_bytes_sent: i64,
113    pub total_bytes_received: i64,
114    pub first_seen: i64,
115    pub last_seen: i64,
116    pub updated_at: Option<i64>,
117}
118
119/// Parsed status code breakdown from endpoint stats
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct StatusCodeBreakdown {
122    pub status_codes: HashMap<u16, i64>,
123}
124
125impl EndpointStats {
126    /// Parse the status codes JSON field
127    pub fn get_status_code_breakdown(&self) -> Result<StatusCodeBreakdown, serde_json::Error> {
128        if let Some(ref json) = self.status_codes {
129            let map: HashMap<String, i64> = serde_json::from_str(json)?;
130            let status_codes = map
131                .into_iter()
132                .filter_map(|(k, v)| k.parse::<u16>().ok().map(|code| (code, v)))
133                .collect();
134            Ok(StatusCodeBreakdown { status_codes })
135        } else {
136            Ok(StatusCodeBreakdown {
137                status_codes: HashMap::new(),
138            })
139        }
140    }
141
142    /// Set the status codes from a breakdown
143    pub fn set_status_code_breakdown(
144        &mut self,
145        breakdown: &StatusCodeBreakdown,
146    ) -> Result<(), serde_json::Error> {
147        let map: HashMap<String, i64> =
148            breakdown.status_codes.iter().map(|(k, v)| (k.to_string(), *v)).collect();
149        self.status_codes = Some(serde_json::to_string(&map)?);
150        Ok(())
151    }
152}
153
154/// Individual error event
155#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
156pub struct ErrorEvent {
157    pub id: Option<i64>,
158    pub timestamp: i64,
159    pub protocol: String,
160    pub method: Option<String>,
161    pub endpoint: Option<String>,
162    pub status_code: Option<i32>,
163    pub error_type: Option<String>,
164    pub error_message: Option<String>,
165    pub error_category: Option<String>,
166    pub request_id: Option<String>,
167    pub trace_id: Option<String>,
168    pub span_id: Option<String>,
169    pub client_ip: Option<String>,
170    pub user_agent: Option<String>,
171    pub workspace_id: Option<String>,
172    pub environment: Option<String>,
173    pub metadata: Option<String>, // JSON
174    pub created_at: Option<i64>,
175}
176
177/// Error category enumeration
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case")]
180pub enum ErrorCategory {
181    ClientError, // 4xx
182    ServerError, // 5xx
183    NetworkError,
184    TimeoutError,
185    Other,
186}
187
188impl ErrorCategory {
189    /// Get the category from a status code
190    #[must_use]
191    pub const fn from_status_code(status_code: u16) -> Self {
192        match status_code {
193            400..=499 => Self::ClientError,
194            500..=599 => Self::ServerError,
195            _ => Self::Other,
196        }
197    }
198
199    /// Convert to string representation
200    #[must_use]
201    pub const fn as_str(&self) -> &'static str {
202        match self {
203            Self::ClientError => "client_error",
204            Self::ServerError => "server_error",
205            Self::NetworkError => "network_error",
206            Self::TimeoutError => "timeout_error",
207            Self::Other => "other",
208        }
209    }
210}
211
212/// Client analytics data
213#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
214pub struct ClientAnalytics {
215    pub id: Option<i64>,
216    pub timestamp: i64,
217    pub client_ip: String,
218    pub user_agent: Option<String>,
219    pub user_agent_family: Option<String>,
220    pub user_agent_version: Option<String>,
221    pub protocol: String,
222    pub workspace_id: Option<String>,
223    pub environment: Option<String>,
224    pub request_count: i64,
225    pub error_count: i64,
226    pub avg_latency_ms: Option<f64>,
227    pub bytes_sent: i64,
228    pub bytes_received: i64,
229    pub top_endpoints: Option<String>, // JSON array
230    pub created_at: Option<i64>,
231}
232
233/// Traffic pattern data for heatmap visualization
234#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
235pub struct TrafficPattern {
236    pub id: Option<i64>,
237    pub date: String,
238    pub hour: i32,
239    pub day_of_week: i32,
240    pub protocol: String,
241    pub workspace_id: Option<String>,
242    pub environment: Option<String>,
243    pub request_count: i64,
244    pub error_count: i64,
245    pub avg_latency_ms: Option<f64>,
246    pub unique_clients: Option<i64>,
247    pub created_at: Option<i64>,
248}
249
250/// Analytics snapshot for comparison and trending
251#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
252pub struct AnalyticsSnapshot {
253    pub id: Option<i64>,
254    pub timestamp: i64,
255    pub snapshot_type: String,
256    pub total_requests: i64,
257    pub total_errors: i64,
258    pub avg_latency_ms: Option<f64>,
259    pub active_connections: Option<i64>,
260    pub protocol_stats: Option<String>, // JSON
261    pub top_endpoints: Option<String>,  // JSON array
262    pub memory_usage_bytes: Option<i64>,
263    pub cpu_usage_percent: Option<f64>,
264    pub thread_count: Option<i32>,
265    pub uptime_seconds: Option<i64>,
266    pub workspace_id: Option<String>,
267    pub environment: Option<String>,
268    pub created_at: Option<i64>,
269}
270
271/// Query filter for analytics queries
272#[derive(Debug, Clone, Default, Serialize, Deserialize)]
273pub struct AnalyticsFilter {
274    pub start_time: Option<i64>,
275    pub end_time: Option<i64>,
276    pub protocol: Option<String>,
277    pub endpoint: Option<String>,
278    pub method: Option<String>,
279    pub status_code: Option<i32>,
280    pub workspace_id: Option<String>,
281    pub environment: Option<String>,
282    pub limit: Option<i64>,
283}
284
285/// Overview metrics for the dashboard
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct OverviewMetrics {
288    pub total_requests: i64,
289    pub total_errors: i64,
290    pub error_rate: f64,
291    pub avg_latency_ms: f64,
292    pub p95_latency_ms: f64,
293    pub p99_latency_ms: f64,
294    pub active_connections: i64,
295    pub total_bytes_sent: i64,
296    pub total_bytes_received: i64,
297    pub requests_per_second: f64,
298    pub top_protocols: Vec<ProtocolStat>,
299    pub top_endpoints: Vec<EndpointStat>,
300}
301
302/// Protocol statistics
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ProtocolStat {
305    pub protocol: String,
306    pub request_count: i64,
307    pub error_count: i64,
308    pub avg_latency_ms: f64,
309}
310
311/// Endpoint statistics summary
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct EndpointStat {
314    pub endpoint: String,
315    pub protocol: String,
316    pub method: Option<String>,
317    pub request_count: i64,
318    pub error_count: i64,
319    pub error_rate: f64,
320    pub avg_latency_ms: f64,
321    pub p95_latency_ms: f64,
322}
323
324/// Time series data point
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct TimeSeriesPoint {
327    pub timestamp: i64,
328    pub value: f64,
329}
330
331/// Time series with metadata
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct TimeSeries {
334    pub label: String,
335    pub data: Vec<TimeSeriesPoint>,
336}
337
338/// Latency percentiles over time
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct LatencyTrend {
341    pub timestamp: i64,
342    pub p50: f64,
343    pub p95: f64,
344    pub p99: f64,
345    pub avg: f64,
346    pub min: f64,
347    pub max: f64,
348}
349
350/// Error summary
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct ErrorSummary {
353    pub error_type: String,
354    pub error_category: String,
355    pub count: i64,
356    pub endpoints: Vec<String>,
357    pub last_occurrence: DateTime<Utc>,
358}
359
360/// Export format
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
362#[serde(rename_all = "lowercase")]
363pub enum ExportFormat {
364    Csv,
365    Json,
366}
367
368// ============================================================================
369// Coverage Metrics Models (MockOps)
370// ============================================================================
371
372/// Scenario usage metrics
373#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
374pub struct ScenarioUsageMetrics {
375    pub id: Option<i64>,
376    pub scenario_id: String,
377    pub workspace_id: Option<String>,
378    pub org_id: Option<String>,
379    pub usage_count: i64,
380    pub last_used_at: Option<i64>,
381    pub usage_pattern: Option<String>, // JSON string
382    pub created_at: Option<i64>,
383    pub updated_at: Option<i64>,
384}
385
386/// Persona CI hit record
387#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
388pub struct PersonaCIHit {
389    pub id: Option<i64>,
390    pub persona_id: String,
391    pub workspace_id: Option<String>,
392    pub org_id: Option<String>,
393    pub ci_run_id: Option<String>,
394    pub hit_count: i64,
395    pub hit_at: i64,
396    pub created_at: Option<i64>,
397}
398
399/// Endpoint coverage metrics
400#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
401pub struct EndpointCoverage {
402    pub id: Option<i64>,
403    pub endpoint: String,
404    pub method: Option<String>,
405    pub protocol: String,
406    pub workspace_id: Option<String>,
407    pub org_id: Option<String>,
408    pub test_count: i64,
409    pub last_tested_at: Option<i64>,
410    pub coverage_percentage: Option<f64>,
411    pub created_at: Option<i64>,
412    pub updated_at: Option<i64>,
413}
414
415/// Reality level staleness record
416#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
417pub struct RealityLevelStaleness {
418    pub id: Option<i64>,
419    pub workspace_id: String,
420    pub org_id: Option<String>,
421    pub endpoint: Option<String>,
422    pub method: Option<String>,
423    pub protocol: Option<String>,
424    pub current_reality_level: Option<String>,
425    pub last_updated_at: Option<i64>,
426    pub staleness_days: Option<i32>,
427    pub created_at: Option<i64>,
428    pub updated_at: Option<i64>,
429}
430
431/// Drift percentage metrics
432#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
433pub struct DriftPercentageMetrics {
434    pub id: Option<i64>,
435    pub workspace_id: String,
436    pub org_id: Option<String>,
437    pub total_mocks: i64,
438    pub drifting_mocks: i64,
439    pub drift_percentage: f64,
440    pub measured_at: i64,
441    pub created_at: Option<i64>,
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    // ==================== Granularity Tests ====================
449
450    #[test]
451    fn test_granularity_serialize() {
452        let minute = Granularity::Minute;
453        let hour = Granularity::Hour;
454        let day = Granularity::Day;
455
456        assert_eq!(serde_json::to_string(&minute).unwrap(), "\"minute\"");
457        assert_eq!(serde_json::to_string(&hour).unwrap(), "\"hour\"");
458        assert_eq!(serde_json::to_string(&day).unwrap(), "\"day\"");
459    }
460
461    #[test]
462    fn test_granularity_deserialize() {
463        let minute: Granularity = serde_json::from_str("\"minute\"").unwrap();
464        let hour: Granularity = serde_json::from_str("\"hour\"").unwrap();
465        let day: Granularity = serde_json::from_str("\"day\"").unwrap();
466
467        assert_eq!(minute, Granularity::Minute);
468        assert_eq!(hour, Granularity::Hour);
469        assert_eq!(day, Granularity::Day);
470    }
471
472    #[test]
473    fn test_granularity_clone() {
474        let g = Granularity::Hour;
475        let cloned = g.clone();
476        assert_eq!(g, cloned);
477    }
478
479    // ==================== ErrorCategory Tests ====================
480
481    #[test]
482    fn test_error_category_from_status_code_client() {
483        assert_eq!(ErrorCategory::from_status_code(400), ErrorCategory::ClientError);
484        assert_eq!(ErrorCategory::from_status_code(401), ErrorCategory::ClientError);
485        assert_eq!(ErrorCategory::from_status_code(403), ErrorCategory::ClientError);
486        assert_eq!(ErrorCategory::from_status_code(404), ErrorCategory::ClientError);
487        assert_eq!(ErrorCategory::from_status_code(499), ErrorCategory::ClientError);
488    }
489
490    #[test]
491    fn test_error_category_from_status_code_server() {
492        assert_eq!(ErrorCategory::from_status_code(500), ErrorCategory::ServerError);
493        assert_eq!(ErrorCategory::from_status_code(502), ErrorCategory::ServerError);
494        assert_eq!(ErrorCategory::from_status_code(503), ErrorCategory::ServerError);
495        assert_eq!(ErrorCategory::from_status_code(504), ErrorCategory::ServerError);
496        assert_eq!(ErrorCategory::from_status_code(599), ErrorCategory::ServerError);
497    }
498
499    #[test]
500    fn test_error_category_from_status_code_other() {
501        assert_eq!(ErrorCategory::from_status_code(200), ErrorCategory::Other);
502        assert_eq!(ErrorCategory::from_status_code(301), ErrorCategory::Other);
503        assert_eq!(ErrorCategory::from_status_code(0), ErrorCategory::Other);
504    }
505
506    #[test]
507    fn test_error_category_as_str() {
508        assert_eq!(ErrorCategory::ClientError.as_str(), "client_error");
509        assert_eq!(ErrorCategory::ServerError.as_str(), "server_error");
510        assert_eq!(ErrorCategory::NetworkError.as_str(), "network_error");
511        assert_eq!(ErrorCategory::TimeoutError.as_str(), "timeout_error");
512        assert_eq!(ErrorCategory::Other.as_str(), "other");
513    }
514
515    #[test]
516    fn test_error_category_serialize() {
517        assert_eq!(serde_json::to_string(&ErrorCategory::ClientError).unwrap(), "\"client_error\"");
518        assert_eq!(serde_json::to_string(&ErrorCategory::ServerError).unwrap(), "\"server_error\"");
519    }
520
521    // ==================== ExportFormat Tests ====================
522
523    #[test]
524    fn test_export_format_serialize() {
525        assert_eq!(serde_json::to_string(&ExportFormat::Csv).unwrap(), "\"csv\"");
526        assert_eq!(serde_json::to_string(&ExportFormat::Json).unwrap(), "\"json\"");
527    }
528
529    #[test]
530    fn test_export_format_deserialize() {
531        let csv: ExportFormat = serde_json::from_str("\"csv\"").unwrap();
532        let json_fmt: ExportFormat = serde_json::from_str("\"json\"").unwrap();
533
534        assert_eq!(csv, ExportFormat::Csv);
535        assert_eq!(json_fmt, ExportFormat::Json);
536    }
537
538    // ==================== EndpointStats Tests ====================
539
540    #[test]
541    fn test_endpoint_stats_get_status_code_breakdown() {
542        let stats = EndpointStats {
543            id: Some(1),
544            endpoint: "/api/users".to_string(),
545            protocol: "http".to_string(),
546            method: Some("GET".to_string()),
547            workspace_id: None,
548            environment: None,
549            total_requests: 100,
550            total_errors: 5,
551            avg_latency_ms: Some(50.0),
552            min_latency_ms: Some(10.0),
553            max_latency_ms: Some(200.0),
554            p95_latency_ms: Some(150.0),
555            status_codes: Some(r#"{"200": 90, "404": 5, "500": 5}"#.to_string()),
556            total_bytes_sent: 10000,
557            total_bytes_received: 5000,
558            first_seen: 1000,
559            last_seen: 2000,
560            updated_at: None,
561        };
562
563        let breakdown = stats.get_status_code_breakdown().unwrap();
564        assert_eq!(breakdown.status_codes.get(&200), Some(&90));
565        assert_eq!(breakdown.status_codes.get(&404), Some(&5));
566        assert_eq!(breakdown.status_codes.get(&500), Some(&5));
567    }
568
569    #[test]
570    fn test_endpoint_stats_get_status_code_breakdown_none() {
571        let stats = EndpointStats {
572            id: None,
573            endpoint: "/api/test".to_string(),
574            protocol: "http".to_string(),
575            method: None,
576            workspace_id: None,
577            environment: None,
578            total_requests: 0,
579            total_errors: 0,
580            avg_latency_ms: None,
581            min_latency_ms: None,
582            max_latency_ms: None,
583            p95_latency_ms: None,
584            status_codes: None,
585            total_bytes_sent: 0,
586            total_bytes_received: 0,
587            first_seen: 0,
588            last_seen: 0,
589            updated_at: None,
590        };
591
592        let breakdown = stats.get_status_code_breakdown().unwrap();
593        assert!(breakdown.status_codes.is_empty());
594    }
595
596    #[test]
597    fn test_endpoint_stats_set_status_code_breakdown() {
598        let mut stats = EndpointStats {
599            id: None,
600            endpoint: "/api/test".to_string(),
601            protocol: "http".to_string(),
602            method: None,
603            workspace_id: None,
604            environment: None,
605            total_requests: 0,
606            total_errors: 0,
607            avg_latency_ms: None,
608            min_latency_ms: None,
609            max_latency_ms: None,
610            p95_latency_ms: None,
611            status_codes: None,
612            total_bytes_sent: 0,
613            total_bytes_received: 0,
614            first_seen: 0,
615            last_seen: 0,
616            updated_at: None,
617        };
618
619        let breakdown = StatusCodeBreakdown {
620            status_codes: HashMap::from([(200, 100), (500, 10)]),
621        };
622
623        stats.set_status_code_breakdown(&breakdown).unwrap();
624        assert!(stats.status_codes.is_some());
625
626        // Verify roundtrip
627        let restored = stats.get_status_code_breakdown().unwrap();
628        assert_eq!(restored.status_codes.get(&200), Some(&100));
629        assert_eq!(restored.status_codes.get(&500), Some(&10));
630    }
631
632    // ==================== AnalyticsFilter Tests ====================
633
634    #[test]
635    fn test_analytics_filter_default() {
636        let filter = AnalyticsFilter::default();
637        assert!(filter.start_time.is_none());
638        assert!(filter.end_time.is_none());
639        assert!(filter.protocol.is_none());
640        assert!(filter.endpoint.is_none());
641        assert!(filter.limit.is_none());
642    }
643
644    #[test]
645    fn test_analytics_filter_serialize() {
646        let filter = AnalyticsFilter {
647            start_time: Some(1000),
648            end_time: Some(2000),
649            protocol: Some("http".to_string()),
650            endpoint: Some("/api/users".to_string()),
651            method: Some("GET".to_string()),
652            status_code: Some(200),
653            workspace_id: None,
654            environment: None,
655            limit: Some(100),
656        };
657
658        let json = serde_json::to_string(&filter).unwrap();
659        assert!(json.contains("1000"));
660        assert!(json.contains("http"));
661        assert!(json.contains("/api/users"));
662    }
663
664    #[test]
665    fn test_analytics_filter_clone() {
666        let filter = AnalyticsFilter {
667            start_time: Some(1000),
668            protocol: Some("grpc".to_string()),
669            ..Default::default()
670        };
671
672        let cloned = filter.clone();
673        assert_eq!(filter.start_time, cloned.start_time);
674        assert_eq!(filter.protocol, cloned.protocol);
675    }
676
677    // ==================== OverviewMetrics Tests ====================
678
679    #[test]
680    fn test_overview_metrics_serialize() {
681        let metrics = OverviewMetrics {
682            total_requests: 1000,
683            total_errors: 50,
684            error_rate: 0.05,
685            avg_latency_ms: 100.0,
686            p95_latency_ms: 250.0,
687            p99_latency_ms: 500.0,
688            active_connections: 10,
689            total_bytes_sent: 100000,
690            total_bytes_received: 50000,
691            requests_per_second: 10.5,
692            top_protocols: vec![],
693            top_endpoints: vec![],
694        };
695
696        let json = serde_json::to_string(&metrics).unwrap();
697        assert!(json.contains("1000"));
698        assert!(json.contains("0.05"));
699    }
700
701    // ==================== ProtocolStat Tests ====================
702
703    #[test]
704    fn test_protocol_stat_serialize() {
705        let stat = ProtocolStat {
706            protocol: "http".to_string(),
707            request_count: 1000,
708            error_count: 10,
709            avg_latency_ms: 50.0,
710        };
711
712        let json = serde_json::to_string(&stat).unwrap();
713        assert!(json.contains("http"));
714        assert!(json.contains("1000"));
715    }
716
717    // ==================== EndpointStat Tests ====================
718
719    #[test]
720    fn test_endpoint_stat_serialize() {
721        let stat = EndpointStat {
722            endpoint: "/api/users".to_string(),
723            protocol: "http".to_string(),
724            method: Some("GET".to_string()),
725            request_count: 500,
726            error_count: 5,
727            error_rate: 0.01,
728            avg_latency_ms: 75.0,
729            p95_latency_ms: 150.0,
730        };
731
732        let json = serde_json::to_string(&stat).unwrap();
733        assert!(json.contains("/api/users"));
734        assert!(json.contains("GET"));
735    }
736
737    // ==================== TimeSeriesPoint Tests ====================
738
739    #[test]
740    fn test_time_series_point_serialize() {
741        let point = TimeSeriesPoint {
742            timestamp: 1234567890,
743            value: 42.5,
744        };
745
746        let json = serde_json::to_string(&point).unwrap();
747        assert!(json.contains("1234567890"));
748        assert!(json.contains("42.5"));
749    }
750
751    // ==================== TimeSeries Tests ====================
752
753    #[test]
754    fn test_time_series_serialize() {
755        let series = TimeSeries {
756            label: "requests".to_string(),
757            data: vec![
758                TimeSeriesPoint {
759                    timestamp: 1000,
760                    value: 10.0,
761                },
762                TimeSeriesPoint {
763                    timestamp: 2000,
764                    value: 20.0,
765                },
766            ],
767        };
768
769        let json = serde_json::to_string(&series).unwrap();
770        assert!(json.contains("requests"));
771        assert!(json.contains("1000"));
772    }
773
774    // ==================== LatencyTrend Tests ====================
775
776    #[test]
777    fn test_latency_trend_serialize() {
778        let trend = LatencyTrend {
779            timestamp: 1000,
780            p50: 50.0,
781            p95: 150.0,
782            p99: 250.0,
783            avg: 75.0,
784            min: 10.0,
785            max: 500.0,
786        };
787
788        let json = serde_json::to_string(&trend).unwrap();
789        assert!(json.contains("p50"));
790        assert!(json.contains("p95"));
791        assert!(json.contains("p99"));
792    }
793
794    // ==================== MetricsAggregate Tests ====================
795
796    #[test]
797    fn test_metrics_aggregate_clone() {
798        let agg = MetricsAggregate {
799            id: Some(1),
800            timestamp: 1000,
801            protocol: "http".to_string(),
802            method: Some("GET".to_string()),
803            endpoint: Some("/api/users".to_string()),
804            status_code: Some(200),
805            workspace_id: None,
806            environment: None,
807            request_count: 100,
808            error_count: 5,
809            latency_sum: 5000.0,
810            latency_min: Some(10.0),
811            latency_max: Some(200.0),
812            latency_p50: Some(50.0),
813            latency_p95: Some(150.0),
814            latency_p99: Some(180.0),
815            bytes_sent: 10000,
816            bytes_received: 5000,
817            active_connections: Some(10),
818            created_at: None,
819        };
820
821        let cloned = agg.clone();
822        assert_eq!(agg.timestamp, cloned.timestamp);
823        assert_eq!(agg.request_count, cloned.request_count);
824    }
825
826    // ==================== ErrorEvent Tests ====================
827
828    #[test]
829    fn test_error_event_serialize() {
830        let event = ErrorEvent {
831            id: Some(1),
832            timestamp: 1000,
833            protocol: "http".to_string(),
834            method: Some("POST".to_string()),
835            endpoint: Some("/api/orders".to_string()),
836            status_code: Some(500),
837            error_type: Some("InternalServerError".to_string()),
838            error_message: Some("Database connection failed".to_string()),
839            error_category: Some("server_error".to_string()),
840            request_id: Some("req-123".to_string()),
841            trace_id: None,
842            span_id: None,
843            client_ip: Some("192.168.1.1".to_string()),
844            user_agent: Some("TestClient/1.0".to_string()),
845            workspace_id: None,
846            environment: None,
847            metadata: None,
848            created_at: None,
849        };
850
851        let json = serde_json::to_string(&event).unwrap();
852        assert!(json.contains("InternalServerError"));
853        assert!(json.contains("Database connection failed"));
854    }
855
856    // ==================== TrafficPattern Tests ====================
857
858    #[test]
859    fn test_traffic_pattern_serialize() {
860        let pattern = TrafficPattern {
861            id: Some(1),
862            date: "2024-01-15".to_string(),
863            hour: 14,
864            day_of_week: 1,
865            protocol: "http".to_string(),
866            workspace_id: None,
867            environment: None,
868            request_count: 500,
869            error_count: 10,
870            avg_latency_ms: Some(50.0),
871            unique_clients: Some(25),
872            created_at: None,
873        };
874
875        let json = serde_json::to_string(&pattern).unwrap();
876        assert!(json.contains("2024-01-15"));
877        assert!(json.contains("14"));
878    }
879
880    // ==================== DriftPercentageMetrics Tests ====================
881
882    #[test]
883    fn test_drift_percentage_metrics_serialize() {
884        let metrics = DriftPercentageMetrics {
885            id: Some(1),
886            workspace_id: "ws-123".to_string(),
887            org_id: Some("org-456".to_string()),
888            total_mocks: 100,
889            drifting_mocks: 15,
890            drift_percentage: 15.0,
891            measured_at: 1000,
892            created_at: None,
893        };
894
895        let json = serde_json::to_string(&metrics).unwrap();
896        assert!(json.contains("ws-123"));
897        assert!(json.contains("15.0"));
898    }
899}