Skip to main content

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-level granularity (60 seconds)
12    Minute,
13    /// Hour-level granularity (3600 seconds)
14    Hour,
15    /// Day-level granularity (86400 seconds)
16    Day,
17}
18
19/// Aggregated metrics for a specific time window
20#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
21#[allow(clippy::missing_docs_in_private_items)]
22pub struct MetricsAggregate {
23    /// Row ID
24    pub id: Option<i64>,
25    /// Unix timestamp for the aggregation window
26    pub timestamp: i64,
27    /// Protocol name (e.g., "http", "grpc")
28    pub protocol: String,
29    /// HTTP method (e.g., "GET", "POST")
30    pub method: Option<String>,
31    /// Endpoint path
32    pub endpoint: Option<String>,
33    /// HTTP status code
34    pub status_code: Option<i32>,
35    /// Workspace identifier
36    pub workspace_id: Option<String>,
37    /// Deployment environment
38    pub environment: Option<String>,
39    /// Total request count in this window
40    pub request_count: i64,
41    /// Total error count in this window
42    pub error_count: i64,
43    /// Sum of all latencies in milliseconds
44    pub latency_sum: f64,
45    /// Minimum latency in milliseconds
46    pub latency_min: Option<f64>,
47    /// Maximum latency in milliseconds
48    pub latency_max: Option<f64>,
49    /// 50th percentile latency
50    pub latency_p50: Option<f64>,
51    /// 95th percentile latency
52    pub latency_p95: Option<f64>,
53    /// 99th percentile latency
54    pub latency_p99: Option<f64>,
55    /// Total bytes sent
56    pub bytes_sent: i64,
57    /// Total bytes received
58    pub bytes_received: i64,
59    /// Number of active connections
60    pub active_connections: Option<i64>,
61    /// Row creation timestamp
62    pub created_at: Option<i64>,
63}
64
65/// Hour-level aggregated metrics
66#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
67pub struct HourMetricsAggregate {
68    /// Row ID
69    pub id: Option<i64>,
70    /// Unix timestamp for the hour window
71    pub timestamp: i64,
72    /// Protocol name
73    pub protocol: String,
74    /// HTTP method
75    pub method: Option<String>,
76    /// Endpoint path
77    pub endpoint: Option<String>,
78    /// HTTP status code
79    pub status_code: Option<i32>,
80    /// Workspace identifier
81    pub workspace_id: Option<String>,
82    /// Deployment environment
83    pub environment: Option<String>,
84    /// Total request count
85    pub request_count: i64,
86    /// Total error count
87    pub error_count: i64,
88    /// Sum of all latencies
89    pub latency_sum: f64,
90    /// Minimum latency
91    pub latency_min: Option<f64>,
92    /// Maximum latency
93    pub latency_max: Option<f64>,
94    /// 50th percentile latency
95    pub latency_p50: Option<f64>,
96    /// 95th percentile latency
97    pub latency_p95: Option<f64>,
98    /// 99th percentile latency
99    pub latency_p99: Option<f64>,
100    /// Total bytes sent
101    pub bytes_sent: i64,
102    /// Total bytes received
103    pub bytes_received: i64,
104    /// Average active connections during the hour
105    pub active_connections_avg: Option<f64>,
106    /// Maximum active connections during the hour
107    pub active_connections_max: Option<i64>,
108    /// Row creation timestamp
109    pub created_at: Option<i64>,
110}
111
112/// Day-level aggregated metrics
113#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
114pub struct DayMetricsAggregate {
115    /// Row ID
116    pub id: Option<i64>,
117    /// Date string (YYYY-MM-DD)
118    pub date: String,
119    /// Unix timestamp for the day
120    pub timestamp: i64,
121    /// Protocol name
122    pub protocol: String,
123    /// HTTP method
124    pub method: Option<String>,
125    /// Endpoint path
126    pub endpoint: Option<String>,
127    /// HTTP status code
128    pub status_code: Option<i32>,
129    /// Workspace identifier
130    pub workspace_id: Option<String>,
131    /// Deployment environment
132    pub environment: Option<String>,
133    /// Total request count
134    pub request_count: i64,
135    /// Total error count
136    pub error_count: i64,
137    /// Sum of all latencies
138    pub latency_sum: f64,
139    /// Minimum latency
140    pub latency_min: Option<f64>,
141    /// Maximum latency
142    pub latency_max: Option<f64>,
143    /// 50th percentile latency
144    pub latency_p50: Option<f64>,
145    /// 95th percentile latency
146    pub latency_p95: Option<f64>,
147    /// 99th percentile latency
148    pub latency_p99: Option<f64>,
149    /// Total bytes sent
150    pub bytes_sent: i64,
151    /// Total bytes received
152    pub bytes_received: i64,
153    /// Average active connections
154    pub active_connections_avg: Option<f64>,
155    /// Maximum active connections
156    pub active_connections_max: Option<i64>,
157    /// Number of unique clients
158    pub unique_clients: Option<i64>,
159    /// Hour with the most requests (0-23)
160    pub peak_hour: Option<i32>,
161    /// Row creation timestamp
162    pub created_at: Option<i64>,
163}
164
165/// Statistics for a specific endpoint
166#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
167pub struct EndpointStats {
168    /// Row ID
169    pub id: Option<i64>,
170    /// Endpoint path
171    pub endpoint: String,
172    /// Protocol name
173    pub protocol: String,
174    /// HTTP method
175    pub method: Option<String>,
176    /// Workspace identifier
177    pub workspace_id: Option<String>,
178    /// Deployment environment
179    pub environment: Option<String>,
180    /// Total number of requests
181    pub total_requests: i64,
182    /// Total number of errors
183    pub total_errors: i64,
184    /// Average latency in milliseconds
185    pub avg_latency_ms: Option<f64>,
186    /// Minimum latency in milliseconds
187    pub min_latency_ms: Option<f64>,
188    /// Maximum latency in milliseconds
189    pub max_latency_ms: Option<f64>,
190    /// 95th percentile latency in milliseconds
191    pub p95_latency_ms: Option<f64>,
192    /// JSON-encoded status code breakdown
193    pub status_codes: Option<String>,
194    /// Total bytes sent
195    pub total_bytes_sent: i64,
196    /// Total bytes received
197    pub total_bytes_received: i64,
198    /// Unix timestamp of first request
199    pub first_seen: i64,
200    /// Unix timestamp of most recent request
201    pub last_seen: i64,
202    /// Last update timestamp
203    pub updated_at: Option<i64>,
204}
205
206/// Parsed status code breakdown from endpoint stats
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct StatusCodeBreakdown {
209    /// Map of HTTP status codes to their occurrence counts
210    pub status_codes: HashMap<u16, i64>,
211}
212
213impl EndpointStats {
214    /// Parse the status codes JSON field
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the JSON status codes field cannot be deserialized.
219    pub fn get_status_code_breakdown(&self) -> Result<StatusCodeBreakdown, serde_json::Error> {
220        if let Some(ref json) = self.status_codes {
221            let map: HashMap<String, i64> = serde_json::from_str(json)?;
222            let status_codes = map
223                .into_iter()
224                .filter_map(|(k, v)| k.parse::<u16>().ok().map(|code| (code, v)))
225                .collect();
226            Ok(StatusCodeBreakdown { status_codes })
227        } else {
228            Ok(StatusCodeBreakdown {
229                status_codes: HashMap::new(),
230            })
231        }
232    }
233
234    /// Set the status codes from a breakdown
235    ///
236    /// # Errors
237    ///
238    /// Returns an error if the status codes cannot be serialized to JSON.
239    pub fn set_status_code_breakdown(
240        &mut self,
241        breakdown: &StatusCodeBreakdown,
242    ) -> Result<(), serde_json::Error> {
243        let map: HashMap<String, i64> =
244            breakdown.status_codes.iter().map(|(k, v)| (k.to_string(), *v)).collect();
245        self.status_codes = Some(serde_json::to_string(&map)?);
246        Ok(())
247    }
248}
249
250/// Individual error event
251#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
252pub struct ErrorEvent {
253    /// Row ID
254    pub id: Option<i64>,
255    /// Unix timestamp of the error
256    pub timestamp: i64,
257    /// Protocol name
258    pub protocol: String,
259    /// HTTP method
260    pub method: Option<String>,
261    /// Endpoint path
262    pub endpoint: Option<String>,
263    /// HTTP status code
264    pub status_code: Option<i32>,
265    /// Error type classification
266    pub error_type: Option<String>,
267    /// Human-readable error message
268    pub error_message: Option<String>,
269    /// Error category (`client_error`, `server_error`, etc.)
270    pub error_category: Option<String>,
271    /// Unique request identifier
272    pub request_id: Option<String>,
273    /// Distributed trace identifier
274    pub trace_id: Option<String>,
275    /// Distributed span identifier
276    pub span_id: Option<String>,
277    /// Client IP address
278    pub client_ip: Option<String>,
279    /// Client user agent string
280    pub user_agent: Option<String>,
281    /// Workspace identifier
282    pub workspace_id: Option<String>,
283    /// Deployment environment
284    pub environment: Option<String>,
285    /// JSON-encoded additional metadata
286    pub metadata: Option<String>,
287    /// Row creation timestamp
288    pub created_at: Option<i64>,
289}
290
291/// Error category enumeration
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum ErrorCategory {
295    /// HTTP 4xx client errors
296    ClientError,
297    /// HTTP 5xx server errors
298    ServerError,
299    /// Network-level errors
300    NetworkError,
301    /// Timeout errors
302    TimeoutError,
303    /// Uncategorized errors
304    Other,
305}
306
307impl ErrorCategory {
308    /// Get the category from a status code
309    #[must_use]
310    pub const fn from_status_code(status_code: u16) -> Self {
311        match status_code {
312            400..=499 => Self::ClientError,
313            500..=599 => Self::ServerError,
314            _ => Self::Other,
315        }
316    }
317
318    /// Convert to string representation
319    #[must_use]
320    pub const fn as_str(&self) -> &'static str {
321        match self {
322            Self::ClientError => "client_error",
323            Self::ServerError => "server_error",
324            Self::NetworkError => "network_error",
325            Self::TimeoutError => "timeout_error",
326            Self::Other => "other",
327        }
328    }
329}
330
331/// Client analytics data
332#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
333pub struct ClientAnalytics {
334    /// Row ID
335    pub id: Option<i64>,
336    /// Unix timestamp
337    pub timestamp: i64,
338    /// Client IP address
339    pub client_ip: String,
340    /// Full user agent string
341    pub user_agent: Option<String>,
342    /// Parsed user agent family
343    pub user_agent_family: Option<String>,
344    /// Parsed user agent version
345    pub user_agent_version: Option<String>,
346    /// Protocol name
347    pub protocol: String,
348    /// Workspace identifier
349    pub workspace_id: Option<String>,
350    /// Deployment environment
351    pub environment: Option<String>,
352    /// Total request count
353    pub request_count: i64,
354    /// Total error count
355    pub error_count: i64,
356    /// Average latency in milliseconds
357    pub avg_latency_ms: Option<f64>,
358    /// Total bytes sent
359    pub bytes_sent: i64,
360    /// Total bytes received
361    pub bytes_received: i64,
362    /// JSON array of most-accessed endpoints
363    pub top_endpoints: Option<String>,
364    /// Row creation timestamp
365    pub created_at: Option<i64>,
366}
367
368/// Traffic pattern data for heatmap visualization
369#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
370pub struct TrafficPattern {
371    /// Row ID
372    pub id: Option<i64>,
373    /// Date string (YYYY-MM-DD)
374    pub date: String,
375    /// Hour of day (0-23)
376    pub hour: i32,
377    /// Day of week (0-6, Monday=0)
378    pub day_of_week: i32,
379    /// Protocol name
380    pub protocol: String,
381    /// Workspace identifier
382    pub workspace_id: Option<String>,
383    /// Deployment environment
384    pub environment: Option<String>,
385    /// Total request count
386    pub request_count: i64,
387    /// Total error count
388    pub error_count: i64,
389    /// Average latency in milliseconds
390    pub avg_latency_ms: Option<f64>,
391    /// Number of unique clients
392    pub unique_clients: Option<i64>,
393    /// Row creation timestamp
394    pub created_at: Option<i64>,
395}
396
397/// Analytics snapshot for comparison and trending
398#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
399pub struct AnalyticsSnapshot {
400    /// Row ID
401    pub id: Option<i64>,
402    /// Unix timestamp of the snapshot
403    pub timestamp: i64,
404    /// Snapshot type identifier
405    pub snapshot_type: String,
406    /// Total requests at snapshot time
407    pub total_requests: i64,
408    /// Total errors at snapshot time
409    pub total_errors: i64,
410    /// Average latency in milliseconds
411    pub avg_latency_ms: Option<f64>,
412    /// Number of active connections
413    pub active_connections: Option<i64>,
414    /// JSON-encoded protocol statistics
415    pub protocol_stats: Option<String>,
416    /// JSON array of top endpoints
417    pub top_endpoints: Option<String>,
418    /// Memory usage in bytes
419    pub memory_usage_bytes: Option<i64>,
420    /// CPU usage percentage
421    pub cpu_usage_percent: Option<f64>,
422    /// Number of active threads
423    pub thread_count: Option<i32>,
424    /// Server uptime in seconds
425    pub uptime_seconds: Option<i64>,
426    /// Workspace identifier
427    pub workspace_id: Option<String>,
428    /// Deployment environment
429    pub environment: Option<String>,
430    /// Row creation timestamp
431    pub created_at: Option<i64>,
432}
433
434/// Query filter for analytics queries
435#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436pub struct AnalyticsFilter {
437    /// Start of time range (unix timestamp)
438    pub start_time: Option<i64>,
439    /// End of time range (unix timestamp)
440    pub end_time: Option<i64>,
441    /// Filter by protocol
442    pub protocol: Option<String>,
443    /// Filter by endpoint path
444    pub endpoint: Option<String>,
445    /// Filter by HTTP method
446    pub method: Option<String>,
447    /// Filter by HTTP status code
448    pub status_code: Option<i32>,
449    /// Filter by workspace
450    pub workspace_id: Option<String>,
451    /// Filter by environment
452    pub environment: Option<String>,
453    /// Maximum number of results
454    pub limit: Option<i64>,
455}
456
457/// Overview metrics for the dashboard
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct OverviewMetrics {
460    /// Total number of requests
461    pub total_requests: i64,
462    /// Total number of errors
463    pub total_errors: i64,
464    /// Error rate as a percentage
465    pub error_rate: f64,
466    /// Average latency in milliseconds
467    pub avg_latency_ms: f64,
468    /// 95th percentile latency in milliseconds
469    pub p95_latency_ms: f64,
470    /// 99th percentile latency in milliseconds
471    pub p99_latency_ms: f64,
472    /// Number of active connections
473    pub active_connections: i64,
474    /// Total bytes sent
475    pub total_bytes_sent: i64,
476    /// Total bytes received
477    pub total_bytes_received: i64,
478    /// Requests per second
479    pub requests_per_second: f64,
480    /// Top protocols by request count
481    pub top_protocols: Vec<ProtocolStat>,
482    /// Top endpoints by request count
483    pub top_endpoints: Vec<EndpointStat>,
484}
485
486/// Protocol statistics
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct ProtocolStat {
489    /// Protocol name
490    pub protocol: String,
491    /// Total request count
492    pub request_count: i64,
493    /// Total error count
494    pub error_count: i64,
495    /// Average latency in milliseconds
496    pub avg_latency_ms: f64,
497}
498
499/// Endpoint statistics summary
500#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct EndpointStat {
502    /// Endpoint path
503    pub endpoint: String,
504    /// Protocol name
505    pub protocol: String,
506    /// HTTP method
507    pub method: Option<String>,
508    /// Total request count
509    pub request_count: i64,
510    /// Total error count
511    pub error_count: i64,
512    /// Error rate as a percentage
513    pub error_rate: f64,
514    /// Average latency in milliseconds
515    pub avg_latency_ms: f64,
516    /// 95th percentile latency in milliseconds
517    pub p95_latency_ms: f64,
518}
519
520/// Time series data point
521#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct TimeSeriesPoint {
523    /// Unix timestamp
524    pub timestamp: i64,
525    /// Metric value at this point
526    pub value: f64,
527}
528
529/// Time series with metadata
530#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct TimeSeries {
532    /// Label identifying this series
533    pub label: String,
534    /// Ordered data points
535    pub data: Vec<TimeSeriesPoint>,
536}
537
538/// Latency percentiles over time
539#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct LatencyTrend {
541    /// Unix timestamp
542    pub timestamp: i64,
543    /// 50th percentile latency
544    pub p50: f64,
545    /// 95th percentile latency
546    pub p95: f64,
547    /// 99th percentile latency
548    pub p99: f64,
549    /// Average latency
550    pub avg: f64,
551    /// Minimum latency
552    pub min: f64,
553    /// Maximum latency
554    pub max: f64,
555}
556
557/// Error summary
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct ErrorSummary {
560    /// Error type classification
561    pub error_type: String,
562    /// Error category
563    pub error_category: String,
564    /// Number of occurrences
565    pub count: i64,
566    /// Endpoints affected
567    pub endpoints: Vec<String>,
568    /// Timestamp of most recent occurrence
569    pub last_occurrence: DateTime<Utc>,
570}
571
572/// Export format
573#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
574#[serde(rename_all = "lowercase")]
575pub enum ExportFormat {
576    /// Comma-separated values format
577    Csv,
578    /// JSON format
579    Json,
580}
581
582// ============================================================================
583// Coverage Metrics Models (MockOps)
584// ============================================================================
585
586/// Scenario usage metrics
587#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
588pub struct ScenarioUsageMetrics {
589    /// Row ID
590    pub id: Option<i64>,
591    /// Scenario identifier
592    pub scenario_id: String,
593    /// Workspace identifier
594    pub workspace_id: Option<String>,
595    /// Organization identifier
596    pub org_id: Option<String>,
597    /// Number of times this scenario has been used
598    pub usage_count: i64,
599    /// Unix timestamp of last usage
600    pub last_used_at: Option<i64>,
601    /// JSON-encoded usage pattern data
602    pub usage_pattern: Option<String>,
603    /// Row creation timestamp
604    pub created_at: Option<i64>,
605    /// Last update timestamp
606    pub updated_at: Option<i64>,
607}
608
609/// Persona CI hit record
610#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
611pub struct PersonaCIHit {
612    /// Row ID
613    pub id: Option<i64>,
614    /// Persona identifier
615    pub persona_id: String,
616    /// Workspace identifier
617    pub workspace_id: Option<String>,
618    /// Organization identifier
619    pub org_id: Option<String>,
620    /// CI run identifier
621    pub ci_run_id: Option<String>,
622    /// Number of hits in this CI run
623    pub hit_count: i64,
624    /// Unix timestamp of the hit
625    pub hit_at: i64,
626    /// Row creation timestamp
627    pub created_at: Option<i64>,
628}
629
630/// Endpoint coverage metrics
631#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
632pub struct EndpointCoverage {
633    /// Row ID
634    pub id: Option<i64>,
635    /// Endpoint path
636    pub endpoint: String,
637    /// HTTP method
638    pub method: Option<String>,
639    /// Protocol name
640    pub protocol: String,
641    /// Workspace identifier
642    pub workspace_id: Option<String>,
643    /// Organization identifier
644    pub org_id: Option<String>,
645    /// Number of tests covering this endpoint
646    pub test_count: i64,
647    /// Unix timestamp of last test run
648    pub last_tested_at: Option<i64>,
649    /// Coverage percentage (0-100)
650    pub coverage_percentage: Option<f64>,
651    /// Row creation timestamp
652    pub created_at: Option<i64>,
653    /// Last update timestamp
654    pub updated_at: Option<i64>,
655}
656
657/// Reality level staleness record
658#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
659pub struct RealityLevelStaleness {
660    /// Row ID
661    pub id: Option<i64>,
662    /// Workspace identifier
663    pub workspace_id: String,
664    /// Organization identifier
665    pub org_id: Option<String>,
666    /// Endpoint path
667    pub endpoint: Option<String>,
668    /// HTTP method
669    pub method: Option<String>,
670    /// Protocol name
671    pub protocol: Option<String>,
672    /// Current reality level setting
673    pub current_reality_level: Option<String>,
674    /// Unix timestamp of last reality level update
675    pub last_updated_at: Option<i64>,
676    /// Number of days since last update
677    pub staleness_days: Option<i32>,
678    /// Row creation timestamp
679    pub created_at: Option<i64>,
680    /// Last update timestamp
681    pub updated_at: Option<i64>,
682}
683
684/// Drift percentage metrics
685#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
686pub struct DriftPercentageMetrics {
687    /// Row ID
688    pub id: Option<i64>,
689    /// Workspace identifier
690    pub workspace_id: String,
691    /// Organization identifier
692    pub org_id: Option<String>,
693    /// Total number of mock endpoints
694    pub total_mocks: i64,
695    /// Number of mocks that have drifted from spec
696    pub drifting_mocks: i64,
697    /// Drift percentage (0-100)
698    pub drift_percentage: f64,
699    /// Unix timestamp of measurement
700    pub measured_at: i64,
701    /// Row creation timestamp
702    pub created_at: Option<i64>,
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    // ==================== Granularity Tests ====================
710
711    #[test]
712    fn test_granularity_serialize() {
713        let minute = Granularity::Minute;
714        let hour = Granularity::Hour;
715        let day = Granularity::Day;
716
717        assert_eq!(serde_json::to_string(&minute).unwrap(), "\"minute\"");
718        assert_eq!(serde_json::to_string(&hour).unwrap(), "\"hour\"");
719        assert_eq!(serde_json::to_string(&day).unwrap(), "\"day\"");
720    }
721
722    #[test]
723    fn test_granularity_deserialize() {
724        let minute: Granularity = serde_json::from_str("\"minute\"").unwrap();
725        let hour: Granularity = serde_json::from_str("\"hour\"").unwrap();
726        let day: Granularity = serde_json::from_str("\"day\"").unwrap();
727
728        assert_eq!(minute, Granularity::Minute);
729        assert_eq!(hour, Granularity::Hour);
730        assert_eq!(day, Granularity::Day);
731    }
732
733    #[test]
734    fn test_granularity_copy() {
735        let g = Granularity::Hour;
736        let copied = g;
737        assert_eq!(g, copied);
738    }
739
740    // ==================== ErrorCategory Tests ====================
741
742    #[test]
743    fn test_error_category_from_status_code_client() {
744        assert_eq!(ErrorCategory::from_status_code(400), ErrorCategory::ClientError);
745        assert_eq!(ErrorCategory::from_status_code(401), ErrorCategory::ClientError);
746        assert_eq!(ErrorCategory::from_status_code(403), ErrorCategory::ClientError);
747        assert_eq!(ErrorCategory::from_status_code(404), ErrorCategory::ClientError);
748        assert_eq!(ErrorCategory::from_status_code(499), ErrorCategory::ClientError);
749    }
750
751    #[test]
752    fn test_error_category_from_status_code_server() {
753        assert_eq!(ErrorCategory::from_status_code(500), ErrorCategory::ServerError);
754        assert_eq!(ErrorCategory::from_status_code(502), ErrorCategory::ServerError);
755        assert_eq!(ErrorCategory::from_status_code(503), ErrorCategory::ServerError);
756        assert_eq!(ErrorCategory::from_status_code(504), ErrorCategory::ServerError);
757        assert_eq!(ErrorCategory::from_status_code(599), ErrorCategory::ServerError);
758    }
759
760    #[test]
761    fn test_error_category_from_status_code_other() {
762        assert_eq!(ErrorCategory::from_status_code(200), ErrorCategory::Other);
763        assert_eq!(ErrorCategory::from_status_code(301), ErrorCategory::Other);
764        assert_eq!(ErrorCategory::from_status_code(0), ErrorCategory::Other);
765    }
766
767    #[test]
768    fn test_error_category_as_str() {
769        assert_eq!(ErrorCategory::ClientError.as_str(), "client_error");
770        assert_eq!(ErrorCategory::ServerError.as_str(), "server_error");
771        assert_eq!(ErrorCategory::NetworkError.as_str(), "network_error");
772        assert_eq!(ErrorCategory::TimeoutError.as_str(), "timeout_error");
773        assert_eq!(ErrorCategory::Other.as_str(), "other");
774    }
775
776    #[test]
777    fn test_error_category_serialize() {
778        assert_eq!(serde_json::to_string(&ErrorCategory::ClientError).unwrap(), "\"client_error\"");
779        assert_eq!(serde_json::to_string(&ErrorCategory::ServerError).unwrap(), "\"server_error\"");
780    }
781
782    // ==================== ExportFormat Tests ====================
783
784    #[test]
785    fn test_export_format_serialize() {
786        assert_eq!(serde_json::to_string(&ExportFormat::Csv).unwrap(), "\"csv\"");
787        assert_eq!(serde_json::to_string(&ExportFormat::Json).unwrap(), "\"json\"");
788    }
789
790    #[test]
791    fn test_export_format_deserialize() {
792        let csv: ExportFormat = serde_json::from_str("\"csv\"").unwrap();
793        let json_fmt: ExportFormat = serde_json::from_str("\"json\"").unwrap();
794
795        assert_eq!(csv, ExportFormat::Csv);
796        assert_eq!(json_fmt, ExportFormat::Json);
797    }
798
799    // ==================== EndpointStats Tests ====================
800
801    #[test]
802    fn test_endpoint_stats_get_status_code_breakdown() {
803        let stats = EndpointStats {
804            id: Some(1),
805            endpoint: "/api/users".to_string(),
806            protocol: "http".to_string(),
807            method: Some("GET".to_string()),
808            workspace_id: None,
809            environment: None,
810            total_requests: 100,
811            total_errors: 5,
812            avg_latency_ms: Some(50.0),
813            min_latency_ms: Some(10.0),
814            max_latency_ms: Some(200.0),
815            p95_latency_ms: Some(150.0),
816            status_codes: Some(r#"{"200": 90, "404": 5, "500": 5}"#.to_string()),
817            total_bytes_sent: 10_000,
818            total_bytes_received: 5_000,
819            first_seen: 1000,
820            last_seen: 2000,
821            updated_at: None,
822        };
823
824        let breakdown = stats.get_status_code_breakdown().unwrap();
825        assert_eq!(breakdown.status_codes.get(&200), Some(&90));
826        assert_eq!(breakdown.status_codes.get(&404), Some(&5));
827        assert_eq!(breakdown.status_codes.get(&500), Some(&5));
828    }
829
830    #[test]
831    fn test_endpoint_stats_get_status_code_breakdown_none() {
832        let stats = EndpointStats {
833            id: None,
834            endpoint: "/api/test".to_string(),
835            protocol: "http".to_string(),
836            method: None,
837            workspace_id: None,
838            environment: None,
839            total_requests: 0,
840            total_errors: 0,
841            avg_latency_ms: None,
842            min_latency_ms: None,
843            max_latency_ms: None,
844            p95_latency_ms: None,
845            status_codes: None,
846            total_bytes_sent: 0,
847            total_bytes_received: 0,
848            first_seen: 0,
849            last_seen: 0,
850            updated_at: None,
851        };
852
853        let breakdown = stats.get_status_code_breakdown().unwrap();
854        assert!(breakdown.status_codes.is_empty());
855    }
856
857    #[test]
858    fn test_endpoint_stats_set_status_code_breakdown() {
859        let mut stats = EndpointStats {
860            id: None,
861            endpoint: "/api/test".to_string(),
862            protocol: "http".to_string(),
863            method: None,
864            workspace_id: None,
865            environment: None,
866            total_requests: 0,
867            total_errors: 0,
868            avg_latency_ms: None,
869            min_latency_ms: None,
870            max_latency_ms: None,
871            p95_latency_ms: None,
872            status_codes: None,
873            total_bytes_sent: 0,
874            total_bytes_received: 0,
875            first_seen: 0,
876            last_seen: 0,
877            updated_at: None,
878        };
879
880        let breakdown = StatusCodeBreakdown {
881            status_codes: HashMap::from([(200, 100), (500, 10)]),
882        };
883
884        stats.set_status_code_breakdown(&breakdown).unwrap();
885        assert!(stats.status_codes.is_some());
886
887        // Verify roundtrip
888        let restored = stats.get_status_code_breakdown().unwrap();
889        assert_eq!(restored.status_codes.get(&200), Some(&100));
890        assert_eq!(restored.status_codes.get(&500), Some(&10));
891    }
892
893    // ==================== AnalyticsFilter Tests ====================
894
895    #[test]
896    fn test_analytics_filter_default() {
897        let filter = AnalyticsFilter::default();
898        assert!(filter.start_time.is_none());
899        assert!(filter.end_time.is_none());
900        assert!(filter.protocol.is_none());
901        assert!(filter.endpoint.is_none());
902        assert!(filter.limit.is_none());
903    }
904
905    #[test]
906    fn test_analytics_filter_serialize() {
907        let filter = AnalyticsFilter {
908            start_time: Some(1000),
909            end_time: Some(2000),
910            protocol: Some("http".to_string()),
911            endpoint: Some("/api/users".to_string()),
912            method: Some("GET".to_string()),
913            status_code: Some(200),
914            workspace_id: None,
915            environment: None,
916            limit: Some(100),
917        };
918
919        let json = serde_json::to_string(&filter).unwrap();
920        assert!(json.contains("1000"));
921        assert!(json.contains("http"));
922        assert!(json.contains("/api/users"));
923    }
924
925    #[test]
926    fn test_analytics_filter_clone() {
927        let filter = AnalyticsFilter {
928            start_time: Some(1000),
929            protocol: Some("grpc".to_string()),
930            ..Default::default()
931        };
932
933        let cloned = filter.clone();
934        assert_eq!(filter.start_time, cloned.start_time);
935        assert_eq!(filter.protocol, cloned.protocol);
936    }
937
938    // ==================== OverviewMetrics Tests ====================
939
940    #[test]
941    fn test_overview_metrics_serialize() {
942        let metrics = OverviewMetrics {
943            total_requests: 1000,
944            total_errors: 50,
945            error_rate: 0.05,
946            avg_latency_ms: 100.0,
947            p95_latency_ms: 250.0,
948            p99_latency_ms: 500.0,
949            active_connections: 10,
950            total_bytes_sent: 100_000,
951            total_bytes_received: 50_000,
952            requests_per_second: 10.5,
953            top_protocols: vec![],
954            top_endpoints: vec![],
955        };
956
957        let json = serde_json::to_string(&metrics).unwrap();
958        assert!(json.contains("1000"));
959        assert!(json.contains("0.05"));
960    }
961
962    // ==================== ProtocolStat Tests ====================
963
964    #[test]
965    fn test_protocol_stat_serialize() {
966        let stat = ProtocolStat {
967            protocol: "http".to_string(),
968            request_count: 1000,
969            error_count: 10,
970            avg_latency_ms: 50.0,
971        };
972
973        let json = serde_json::to_string(&stat).unwrap();
974        assert!(json.contains("http"));
975        assert!(json.contains("1000"));
976    }
977
978    // ==================== EndpointStat Tests ====================
979
980    #[test]
981    fn test_endpoint_stat_serialize() {
982        let stat = EndpointStat {
983            endpoint: "/api/users".to_string(),
984            protocol: "http".to_string(),
985            method: Some("GET".to_string()),
986            request_count: 500,
987            error_count: 5,
988            error_rate: 0.01,
989            avg_latency_ms: 75.0,
990            p95_latency_ms: 150.0,
991        };
992
993        let json = serde_json::to_string(&stat).unwrap();
994        assert!(json.contains("/api/users"));
995        assert!(json.contains("GET"));
996    }
997
998    // ==================== TimeSeriesPoint Tests ====================
999
1000    #[test]
1001    fn test_time_series_point_serialize() {
1002        let point = TimeSeriesPoint {
1003            timestamp: 1_234_567_890,
1004            value: 42.5,
1005        };
1006
1007        let json = serde_json::to_string(&point).unwrap();
1008        assert!(json.contains("1234567890"));
1009        assert!(json.contains("42.5"));
1010    }
1011
1012    // ==================== TimeSeries Tests ====================
1013
1014    #[test]
1015    fn test_time_series_serialize() {
1016        let series = TimeSeries {
1017            label: "requests".to_string(),
1018            data: vec![
1019                TimeSeriesPoint {
1020                    timestamp: 1000,
1021                    value: 10.0,
1022                },
1023                TimeSeriesPoint {
1024                    timestamp: 2000,
1025                    value: 20.0,
1026                },
1027            ],
1028        };
1029
1030        let json = serde_json::to_string(&series).unwrap();
1031        assert!(json.contains("requests"));
1032        assert!(json.contains("1000"));
1033    }
1034
1035    // ==================== LatencyTrend Tests ====================
1036
1037    #[test]
1038    fn test_latency_trend_serialize() {
1039        let trend = LatencyTrend {
1040            timestamp: 1000,
1041            p50: 50.0,
1042            p95: 150.0,
1043            p99: 250.0,
1044            avg: 75.0,
1045            min: 10.0,
1046            max: 500.0,
1047        };
1048
1049        let json = serde_json::to_string(&trend).unwrap();
1050        assert!(json.contains("p50"));
1051        assert!(json.contains("p95"));
1052        assert!(json.contains("p99"));
1053    }
1054
1055    // ==================== MetricsAggregate Tests ====================
1056
1057    #[test]
1058    fn test_metrics_aggregate_clone() {
1059        let agg = MetricsAggregate {
1060            id: Some(1),
1061            timestamp: 1000,
1062            protocol: "http".to_string(),
1063            method: Some("GET".to_string()),
1064            endpoint: Some("/api/users".to_string()),
1065            status_code: Some(200),
1066            workspace_id: None,
1067            environment: None,
1068            request_count: 100,
1069            error_count: 5,
1070            latency_sum: 5000.0,
1071            latency_min: Some(10.0),
1072            latency_max: Some(200.0),
1073            latency_p50: Some(50.0),
1074            latency_p95: Some(150.0),
1075            latency_p99: Some(180.0),
1076            bytes_sent: 10000,
1077            bytes_received: 5000,
1078            active_connections: Some(10),
1079            created_at: None,
1080        };
1081
1082        let cloned = agg.clone();
1083        assert_eq!(agg.timestamp, cloned.timestamp);
1084        assert_eq!(agg.request_count, cloned.request_count);
1085    }
1086
1087    // ==================== ErrorEvent Tests ====================
1088
1089    #[test]
1090    fn test_error_event_serialize() {
1091        let event = ErrorEvent {
1092            id: Some(1),
1093            timestamp: 1000,
1094            protocol: "http".to_string(),
1095            method: Some("POST".to_string()),
1096            endpoint: Some("/api/orders".to_string()),
1097            status_code: Some(500),
1098            error_type: Some("InternalServerError".to_string()),
1099            error_message: Some("Database connection failed".to_string()),
1100            error_category: Some("server_error".to_string()),
1101            request_id: Some("req-123".to_string()),
1102            trace_id: None,
1103            span_id: None,
1104            client_ip: Some("192.168.1.1".to_string()),
1105            user_agent: Some("TestClient/1.0".to_string()),
1106            workspace_id: None,
1107            environment: None,
1108            metadata: None,
1109            created_at: None,
1110        };
1111
1112        let json = serde_json::to_string(&event).unwrap();
1113        assert!(json.contains("InternalServerError"));
1114        assert!(json.contains("Database connection failed"));
1115    }
1116
1117    // ==================== TrafficPattern Tests ====================
1118
1119    #[test]
1120    fn test_traffic_pattern_serialize() {
1121        let pattern = TrafficPattern {
1122            id: Some(1),
1123            date: "2024-01-15".to_string(),
1124            hour: 14,
1125            day_of_week: 1,
1126            protocol: "http".to_string(),
1127            workspace_id: None,
1128            environment: None,
1129            request_count: 500,
1130            error_count: 10,
1131            avg_latency_ms: Some(50.0),
1132            unique_clients: Some(25),
1133            created_at: None,
1134        };
1135
1136        let json = serde_json::to_string(&pattern).unwrap();
1137        assert!(json.contains("2024-01-15"));
1138        assert!(json.contains("14"));
1139    }
1140
1141    // ==================== DriftPercentageMetrics Tests ====================
1142
1143    #[test]
1144    fn test_drift_percentage_metrics_serialize() {
1145        let metrics = DriftPercentageMetrics {
1146            id: Some(1),
1147            workspace_id: "ws-123".to_string(),
1148            org_id: Some("org-456".to_string()),
1149            total_mocks: 100,
1150            drifting_mocks: 15,
1151            drift_percentage: 15.0,
1152            measured_at: 1000,
1153            created_at: None,
1154        };
1155
1156        let json = serde_json::to_string(&metrics).unwrap();
1157        assert!(json.contains("ws-123"));
1158        assert!(json.contains("15.0"));
1159    }
1160}