Skip to main content

mockforge_tui/api/
models.rs

1//! Standalone deserialization types mirroring the admin API JSON shapes.
2//! Intentionally duplicated from `mockforge-ui` for decoupling — this crate
3//! has zero internal `mockforge-*` dependencies.
4
5use std::collections::HashMap;
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10// ── API envelope ─────────────────────────────────────────────────────
11
12/// Generic API response wrapper used by all admin endpoints.
13/// The server may include a `timestamp` field which we ignore.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ApiResponse<T> {
16    pub success: bool,
17    pub data: Option<T>,
18    pub error: Option<String>,
19}
20
21// ── Endpoint wrapper types ──────────────────────────────────────────
22// Some endpoints return data in a named wrapper instead of a raw array.
23
24#[derive(Debug, Clone, Deserialize)]
25pub struct RoutesWrapper {
26    #[serde(default)]
27    pub routes: Vec<RouteInfo>,
28}
29
30#[derive(Debug, Clone, Deserialize)]
31pub struct PluginsWrapper {
32    #[serde(default)]
33    pub plugins: Vec<PluginInfo>,
34    #[serde(default)]
35    pub total: u64,
36}
37
38#[derive(Debug, Clone, Deserialize)]
39pub struct ContractDiffWrapper {
40    #[serde(default)]
41    pub captures: Vec<ContractDiffCapture>,
42}
43
44/// Server-side spec-violation entry surfaced by the new Conformance TUI
45/// screen (Issue #79 round 12).
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ConformanceViolation {
48    pub timestamp: DateTime<Utc>,
49    pub method: String,
50    pub path: String,
51    #[serde(default = "default_unknown")]
52    pub client_ip: String,
53    pub status: u16,
54    pub reason: String,
55    #[serde(default)]
56    pub category: String,
57    /// Round 30 — `MOCKFORGE_CONFORMANCE_BUFFER_UNIQUE=true` causes the
58    /// server-side buffer to dedup by signature and stamp every entry
59    /// with how many duplicate hits coalesced into it. Defaults to `1`
60    /// when older mockforge instances send the field-less payload.
61    #[serde(default = "default_occurrences")]
62    pub occurrences: u32,
63}
64
65fn default_unknown() -> String {
66    "unknown".to_string()
67}
68
69fn default_occurrences() -> u32 {
70    1
71}
72
73#[derive(Debug, Clone, Deserialize)]
74pub struct ConformanceViolationsResponse {
75    #[serde(default)]
76    pub violations: Vec<ConformanceViolation>,
77    #[serde(default)]
78    pub total: usize,
79    /// Lifetime count since server start (Issue #79 round 15); the
80    /// buffer caps `total` at 256, this is the true number seen.
81    #[serde(default)]
82    pub total_seen: u64,
83    /// Lifetime count of requests that *passed* the validator (Issue
84    /// #79 round 17.1). With `total_seen` this gives the real pass/fail
85    /// ratio under sustained traffic.
86    #[serde(default)]
87    pub total_ok: u64,
88}
89
90/// One unmatched-path request (Issue #79 round 13).
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct UnknownPathRequest {
93    pub timestamp: DateTime<Utc>,
94    pub method: String,
95    pub path: String,
96    #[serde(default = "default_unknown")]
97    pub client_ip: String,
98    #[serde(default)]
99    pub query: String,
100    /// HTTP status the server returned (404 normally; 200 in shadow
101    /// mode — Issue #79 round 14). Defaults to 404 for older servers.
102    #[serde(default = "default_unknown_status")]
103    pub status: u16,
104}
105
106fn default_unknown_status() -> u16 {
107    404
108}
109
110#[derive(Debug, Clone, Deserialize)]
111pub struct UnknownPathsResponse {
112    #[serde(default)]
113    pub requests: Vec<UnknownPathRequest>,
114    #[serde(default)]
115    pub total: usize,
116    /// Lifetime count since server start (Issue #79 round 15).
117    #[serde(default)]
118    pub total_seen: u64,
119}
120
121// ── Dashboard ────────────────────────────────────────────────────────
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct DashboardData {
125    pub server_info: ServerInfo,
126    pub system_info: DashboardSystemInfo,
127    pub metrics: SimpleMetrics,
128    pub servers: Vec<ServerStatus>,
129    pub recent_logs: Vec<RequestLog>,
130    pub system: SystemInfo,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ServerInfo {
135    #[serde(default)]
136    pub version: String,
137    #[serde(default)]
138    pub build_time: String,
139    #[serde(default)]
140    pub git_sha: String,
141    pub http_server: Option<String>,
142    pub ws_server: Option<String>,
143    pub grpc_server: Option<String>,
144    pub graphql_server: Option<String>,
145    #[serde(default)]
146    pub api_enabled: bool,
147    #[serde(default)]
148    pub admin_port: u16,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct DashboardSystemInfo {
153    #[serde(default)]
154    pub os: String,
155    #[serde(default)]
156    pub arch: String,
157    #[serde(default)]
158    pub uptime: u64,
159    #[serde(default)]
160    pub memory_usage: u64,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct SimpleMetrics {
165    #[serde(default)]
166    pub total_requests: u64,
167    #[serde(default)]
168    pub active_requests: u64,
169    #[serde(default)]
170    pub average_response_time: f64,
171    #[serde(default)]
172    pub error_rate: f64,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ServerStatus {
177    pub server_type: String,
178    pub address: Option<String>,
179    #[serde(default)]
180    pub running: bool,
181    pub start_time: Option<DateTime<Utc>>,
182    pub uptime_seconds: Option<u64>,
183    #[serde(default)]
184    pub active_connections: u64,
185    #[serde(default)]
186    pub total_requests: u64,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct SystemInfo {
191    #[serde(default)]
192    pub version: String,
193    #[serde(default)]
194    pub uptime_seconds: u64,
195    #[serde(default)]
196    pub memory_usage_mb: u64,
197    #[serde(default)]
198    pub cpu_usage_percent: f64,
199    #[serde(default)]
200    pub active_threads: usize,
201    #[serde(default)]
202    pub total_routes: usize,
203    #[serde(default)]
204    pub total_fixtures: usize,
205    #[serde(default)]
206    pub peak_memory_usage_mb: u64,
207    #[serde(default)]
208    pub peak_cpu_usage_percent: f64,
209    #[serde(default)]
210    pub peak_error_rate: f64,
211    #[serde(default)]
212    pub peaks_since: Option<DateTime<Utc>>,
213    /// Successful (200..=399) responses per second, sampled over the last
214    /// metrics interval. "API transactions/sec" in load-testing terms.
215    #[serde(default)]
216    pub tps: f64,
217    #[serde(default)]
218    pub peak_tps: f64,
219    /// 200-OK responses per second over the last metrics interval.
220    #[serde(default)]
221    pub rps_200: f64,
222    #[serde(default)]
223    pub peak_rps_200: f64,
224    /// Accepted TCP connections per second (plain HTTP path; reads 0 for
225    /// HTTPS-only setups).
226    #[serde(default)]
227    pub cps: f64,
228    #[serde(default)]
229    pub peak_cps: f64,
230    /// Currently-open HTTP connections (live gauge). Issue #79 round 6 —
231    /// Srikanth's "how many open at a time" ask.
232    #[serde(default)]
233    pub connections_open: u64,
234    /// Lifetime accepted HTTP connections (cumulative).
235    #[serde(default)]
236    pub connections_total_opened: u64,
237    /// Lifetime closed HTTP connections (cumulative).
238    #[serde(default)]
239    pub connections_total_closed: u64,
240    /// Peak `connections_open` observed since `peaks_since`.
241    #[serde(default)]
242    pub peak_connections_open: u64,
243}
244
245// ── Request logs ─────────────────────────────────────────────────────
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct RequestLog {
249    pub id: String,
250    pub timestamp: DateTime<Utc>,
251    pub method: String,
252    pub path: String,
253    pub status_code: u16,
254    #[serde(default)]
255    pub response_time_ms: u64,
256    pub client_ip: Option<String>,
257    pub user_agent: Option<String>,
258    #[serde(default)]
259    pub headers: HashMap<String, String>,
260    #[serde(default)]
261    pub response_size_bytes: u64,
262    pub error_message: Option<String>,
263}
264
265// ── Routes ───────────────────────────────────────────────────────────
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct RouteInfo {
269    pub method: Option<String>,
270    pub path: String,
271    #[serde(default)]
272    pub priority: i32,
273    #[serde(default)]
274    pub has_fixtures: bool,
275    pub latency_ms: Option<u64>,
276    #[serde(default)]
277    pub request_count: u64,
278    pub last_request: Option<DateTime<Utc>>,
279    #[serde(default)]
280    pub error_count: u64,
281}
282
283// ── Metrics ──────────────────────────────────────────────────────────
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct MetricsData {
287    #[serde(default)]
288    pub requests_by_endpoint: HashMap<String, u64>,
289    #[serde(default)]
290    pub response_time_percentiles: HashMap<String, u64>,
291    pub endpoint_percentiles: Option<HashMap<String, HashMap<String, u64>>>,
292    pub latency_over_time: Option<Vec<(DateTime<Utc>, u64)>>,
293    #[serde(default)]
294    pub error_rate_by_endpoint: HashMap<String, f64>,
295    #[serde(default)]
296    pub memory_usage_over_time: Vec<(DateTime<Utc>, u64)>,
297    #[serde(default)]
298    pub cpu_usage_over_time: Vec<(DateTime<Utc>, f64)>,
299}
300
301// ── Health ───────────────────────────────────────────────────────────
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct HealthCheck {
305    pub status: String,
306    #[serde(default)]
307    pub services: HashMap<String, String>,
308    pub last_check: Option<DateTime<Utc>>,
309    #[serde(default)]
310    pub issues: Vec<String>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct HealthProbe {
315    pub status: String,
316    #[serde(default)]
317    pub checks: HashMap<String, serde_json::Value>,
318}
319
320// ── Config ───────────────────────────────────────────────────────────
321
322#[derive(Debug, Clone, Default, Serialize, Deserialize)]
323pub struct ConfigState {
324    #[serde(default)]
325    pub latency: LatencyConfig,
326    #[serde(default)]
327    pub faults: FaultConfig,
328    #[serde(default)]
329    pub proxy: ProxyConfig,
330    #[serde(default)]
331    pub traffic_shaping: TrafficShapingConfig,
332    #[serde(default)]
333    pub validation: ValidationConfig,
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
337pub struct LatencyConfig {
338    #[serde(default)]
339    pub enabled: bool,
340    #[serde(default)]
341    pub base_ms: u64,
342    #[serde(default)]
343    pub jitter_ms: u64,
344    #[serde(default)]
345    pub tag_overrides: HashMap<String, u64>,
346}
347
348#[derive(Debug, Clone, Default, Serialize, Deserialize)]
349pub struct FaultConfig {
350    #[serde(default)]
351    pub enabled: bool,
352    #[serde(default)]
353    pub failure_rate: f64,
354    #[serde(default)]
355    pub status_codes: Vec<u16>,
356}
357
358#[derive(Debug, Clone, Default, Serialize, Deserialize)]
359pub struct ProxyConfig {
360    #[serde(default)]
361    pub enabled: bool,
362    pub upstream_url: Option<String>,
363    #[serde(default)]
364    pub timeout_seconds: u64,
365}
366
367#[derive(Debug, Clone, Default, Serialize, Deserialize)]
368pub struct TrafficShapingConfig {
369    #[serde(default)]
370    pub enabled: bool,
371    #[serde(default)]
372    pub bandwidth: serde_json::Value,
373    #[serde(default)]
374    pub burst_loss: serde_json::Value,
375}
376
377#[derive(Debug, Clone, Default, Serialize, Deserialize)]
378pub struct ValidationConfig {
379    #[serde(default)]
380    pub mode: String,
381    #[serde(default)]
382    pub aggregate_errors: bool,
383    #[serde(default)]
384    pub validate_responses: bool,
385    #[serde(default)]
386    pub overrides: serde_json::Value,
387}
388
389// ── Plugins ──────────────────────────────────────────────────────────
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct PluginInfo {
393    pub id: String,
394    pub name: String,
395    #[serde(default)]
396    pub version: String,
397    #[serde(default)]
398    pub types: Vec<String>,
399    #[serde(default)]
400    pub status: String,
401    #[serde(default)]
402    pub healthy: bool,
403    #[serde(default)]
404    pub description: String,
405    #[serde(default)]
406    pub author: String,
407}
408
409// ── Fixtures ─────────────────────────────────────────────────────────
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct FixtureInfo {
413    pub id: String,
414    #[serde(default)]
415    pub protocol: String,
416    #[serde(default)]
417    pub method: String,
418    #[serde(default)]
419    pub path: String,
420    pub saved_at: Option<DateTime<Utc>>,
421    #[serde(default)]
422    pub file_size: u64,
423    #[serde(default)]
424    pub file_path: String,
425    #[serde(default)]
426    pub fingerprint: String,
427    #[serde(default)]
428    pub metadata: serde_json::Value,
429}
430
431// ── Smoke tests ──────────────────────────────────────────────────────
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct SmokeTestResult {
435    pub id: String,
436    #[serde(default)]
437    pub name: String,
438    #[serde(default)]
439    pub method: String,
440    #[serde(default)]
441    pub path: String,
442    #[serde(default)]
443    pub description: String,
444    pub last_run: Option<DateTime<Utc>>,
445    #[serde(default)]
446    pub status: String,
447    pub response_time_ms: Option<u64>,
448    pub error_message: Option<String>,
449    pub status_code: Option<u16>,
450    pub duration_seconds: Option<f64>,
451}
452
453// ── Workspaces ───────────────────────────────────────────────────────
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct WorkspaceInfo {
457    pub id: String,
458    #[serde(default)]
459    pub name: String,
460    #[serde(default)]
461    pub description: String,
462    #[serde(default)]
463    pub active: bool,
464    pub created_at: Option<DateTime<Utc>>,
465    #[serde(default)]
466    pub environments: Vec<String>,
467}
468
469// ── Chaos ────────────────────────────────────────────────────────────
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct ChaosStatus {
473    #[serde(default)]
474    pub enabled: bool,
475    pub active_scenario: Option<String>,
476    #[serde(default)]
477    pub settings: serde_json::Value,
478}
479
480// ── Time Travel ──────────────────────────────────────────────────────
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct TimeTravelStatus {
484    #[serde(default)]
485    pub enabled: bool,
486    pub current_time: Option<DateTime<Utc>>,
487    /// Server sends `scale_factor`, older API sends `time_scale`
488    #[serde(default, alias = "time_scale")]
489    pub scale_factor: Option<f64>,
490    pub real_time: Option<DateTime<Utc>>,
491    #[serde(default)]
492    pub scheduled_responses: u64,
493}
494
495// ── Chains ───────────────────────────────────────────────────────────
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct ChainInfo {
499    pub id: String,
500    #[serde(default)]
501    pub name: String,
502    #[serde(default)]
503    pub steps: Vec<serde_json::Value>,
504    #[serde(default)]
505    pub description: String,
506}
507
508// ── Audit ────────────────────────────────────────────────────────────
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct AuditEntry {
512    pub id: String,
513    pub timestamp: Option<DateTime<Utc>>,
514    #[serde(default)]
515    pub action: String,
516    #[serde(default)]
517    pub user: String,
518    #[serde(default)]
519    pub details: serde_json::Value,
520}
521
522// ── Analytics ────────────────────────────────────────────────────────
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct AnalyticsSummary {
526    /// Server sends `request_rate` or `total_requests`
527    #[serde(default, alias = "total_requests")]
528    pub request_rate: f64,
529    #[serde(default)]
530    pub unique_endpoints: u64,
531    /// Server sends `error_rate_percent` or `error_rate`
532    #[serde(default, alias = "error_rate")]
533    pub error_rate_percent: f64,
534    #[serde(default, alias = "avg_response_time")]
535    pub p95_latency_ms: f64,
536    #[serde(default)]
537    pub active_connections: f64,
538    #[serde(default)]
539    pub top_endpoints: Vec<EndpointStat>,
540    /// Server may include a timestamp inside the data
541    #[serde(default)]
542    pub timestamp: Option<String>,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct EndpointStat {
547    #[serde(default)]
548    pub endpoint: String,
549    #[serde(default)]
550    pub count: u64,
551    #[serde(default)]
552    pub avg_time: f64,
553}
554
555// ── Recorder ─────────────────────────────────────────────────────────
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct RecorderStatus {
559    #[serde(default)]
560    pub recording: bool,
561    #[serde(default)]
562    pub recorded_count: u64,
563}
564
565// ── Verification ─────────────────────────────────────────────────────
566
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct VerificationResult {
569    #[serde(default)]
570    pub matched: bool,
571    #[serde(default)]
572    pub count: u64,
573    #[serde(default)]
574    pub details: serde_json::Value,
575}
576
577// ── World State ──────────────────────────────────────────────────────
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct WorldStateEntry {
581    #[serde(default)]
582    pub key: String,
583    #[serde(default)]
584    pub value: serde_json::Value,
585    pub updated_at: Option<DateTime<Utc>>,
586}
587
588// ── Federation ───────────────────────────────────────────────────────
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct FederationPeer {
592    #[serde(default)]
593    pub id: String,
594    #[serde(default)]
595    pub url: String,
596    #[serde(default)]
597    pub status: String,
598    pub last_sync: Option<DateTime<Utc>>,
599}
600
601// ── Contract Diff ────────────────────────────────────────────────────
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct ContractDiffCapture {
605    pub id: String,
606    #[serde(default)]
607    pub path: String,
608    #[serde(default)]
609    pub method: String,
610    #[serde(default)]
611    pub diff_status: String,
612    pub captured_at: Option<DateTime<Utc>>,
613    /// Whether the capture has been analyzed
614    #[serde(default)]
615    pub analyzed: bool,
616    #[serde(default)]
617    pub source: String,
618    #[serde(default)]
619    pub headers: HashMap<String, String>,
620    #[serde(default)]
621    pub query_params: HashMap<String, String>,
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627
628    #[test]
629    fn deserialize_api_response_success() {
630        let json = r#"{
631            "success": true,
632            "data": "hello",
633            "error": null
634        }"#;
635        let resp: ApiResponse<String> = serde_json::from_str(json).unwrap();
636        assert!(resp.success);
637        assert_eq!(resp.data.unwrap(), "hello");
638        assert!(resp.error.is_none());
639    }
640
641    #[test]
642    fn deserialize_api_response_with_timestamp() {
643        // Server includes a timestamp field that TUI ignores
644        let json = r#"{
645            "success": true,
646            "data": "hello",
647            "error": null,
648            "timestamp": "2026-03-07T05:16:29.407667010Z"
649        }"#;
650        let resp: ApiResponse<String> = serde_json::from_str(json).unwrap();
651        assert!(resp.success);
652        assert_eq!(resp.data.unwrap(), "hello");
653    }
654
655    #[test]
656    fn deserialize_api_response_error() {
657        let json = r#"{
658            "success": false,
659            "data": null,
660            "error": "something went wrong"
661        }"#;
662        let resp: ApiResponse<String> = serde_json::from_str(json).unwrap();
663        assert!(!resp.success);
664        assert!(resp.data.is_none());
665        assert_eq!(resp.error.unwrap(), "something went wrong");
666    }
667
668    #[test]
669    fn deserialize_server_info_minimal() {
670        let json = r#"{
671            "version": "0.3.31"
672        }"#;
673        let info: ServerInfo = serde_json::from_str(json).unwrap();
674        assert_eq!(info.version, "0.3.31");
675        assert!(info.build_time.is_empty());
676        assert!(info.git_sha.is_empty());
677        assert!(info.http_server.is_none());
678        assert!(!info.api_enabled);
679        assert_eq!(info.admin_port, 0);
680    }
681
682    #[test]
683    fn deserialize_server_info_full() {
684        let json = r#"{
685            "version": "0.3.31",
686            "build_time": "2025-01-15T10:00:00Z",
687            "git_sha": "abc123",
688            "http_server": "http://0.0.0.0:3000",
689            "ws_server": "ws://0.0.0.0:3001",
690            "grpc_server": null,
691            "graphql_server": null,
692            "api_enabled": true,
693            "admin_port": 9080
694        }"#;
695        let info: ServerInfo = serde_json::from_str(json).unwrap();
696        assert_eq!(info.version, "0.3.31");
697        assert_eq!(info.http_server.unwrap(), "http://0.0.0.0:3000");
698        assert!(info.api_enabled);
699        assert_eq!(info.admin_port, 9080);
700    }
701
702    #[test]
703    fn deserialize_request_log() {
704        let json = r#"{
705            "id": "req-001",
706            "timestamp": "2025-06-15T12:00:00Z",
707            "method": "GET",
708            "path": "/api/users",
709            "status_code": 200,
710            "response_time_ms": 42,
711            "client_ip": "127.0.0.1",
712            "user_agent": "curl/8.0",
713            "headers": {"content-type": "application/json"},
714            "response_size_bytes": 1024,
715            "error_message": null
716        }"#;
717        let log: RequestLog = serde_json::from_str(json).unwrap();
718        assert_eq!(log.id, "req-001");
719        assert_eq!(log.method, "GET");
720        assert_eq!(log.path, "/api/users");
721        assert_eq!(log.status_code, 200);
722        assert_eq!(log.response_time_ms, 42);
723        assert_eq!(log.client_ip.unwrap(), "127.0.0.1");
724        assert_eq!(log.headers.get("content-type").unwrap(), "application/json");
725    }
726
727    #[test]
728    fn deserialize_request_log_minimal() {
729        let json = r#"{
730            "id": "req-002",
731            "timestamp": "2025-06-15T12:00:00Z",
732            "method": "POST",
733            "path": "/api/data",
734            "status_code": 500
735        }"#;
736        let log: RequestLog = serde_json::from_str(json).unwrap();
737        assert_eq!(log.id, "req-002");
738        assert_eq!(log.status_code, 500);
739        assert_eq!(log.response_time_ms, 0);
740        assert!(log.client_ip.is_none());
741        assert!(log.headers.is_empty());
742    }
743
744    #[test]
745    fn deserialize_route_info() {
746        let json = r#"{
747            "method": "GET",
748            "path": "/api/users/{id}",
749            "priority": 10,
750            "has_fixtures": true,
751            "latency_ms": 50,
752            "request_count": 100,
753            "last_request": "2025-06-15T12:30:00Z",
754            "error_count": 2
755        }"#;
756        let route: RouteInfo = serde_json::from_str(json).unwrap();
757        assert_eq!(route.method.unwrap(), "GET");
758        assert_eq!(route.path, "/api/users/{id}");
759        assert_eq!(route.priority, 10);
760        assert!(route.has_fixtures);
761        assert_eq!(route.latency_ms.unwrap(), 50);
762        assert_eq!(route.request_count, 100);
763        assert_eq!(route.error_count, 2);
764    }
765
766    #[test]
767    fn deserialize_route_info_minimal() {
768        let json = r#"{
769            "path": "/health"
770        }"#;
771        let route: RouteInfo = serde_json::from_str(json).unwrap();
772        assert!(route.method.is_none());
773        assert_eq!(route.path, "/health");
774        assert_eq!(route.priority, 0);
775        assert!(!route.has_fixtures);
776        assert!(route.latency_ms.is_none());
777    }
778
779    #[test]
780    fn deserialize_health_check() {
781        let json = r#"{
782            "status": "healthy",
783            "services": {"http": "up", "grpc": "up"},
784            "last_check": "2025-06-15T12:00:00Z",
785            "issues": []
786        }"#;
787        let health: HealthCheck = serde_json::from_str(json).unwrap();
788        assert_eq!(health.status, "healthy");
789        assert_eq!(health.services.len(), 2);
790        assert!(health.issues.is_empty());
791    }
792
793    #[test]
794    fn deserialize_health_check_with_issues() {
795        let json = r#"{
796            "status": "degraded",
797            "issues": ["kafka disconnected", "high latency"]
798        }"#;
799        let health: HealthCheck = serde_json::from_str(json).unwrap();
800        assert_eq!(health.status, "degraded");
801        assert_eq!(health.issues.len(), 2);
802        assert_eq!(health.issues[0], "kafka disconnected");
803    }
804
805    #[test]
806    fn deserialize_plugin_info() {
807        let json = r#"{
808            "id": "plugin-001",
809            "name": "response-graphql",
810            "version": "1.0.0",
811            "types": ["response", "graphql"],
812            "status": "active",
813            "healthy": true,
814            "description": "GraphQL response plugin",
815            "author": "MockForge"
816        }"#;
817        let plugin: PluginInfo = serde_json::from_str(json).unwrap();
818        assert_eq!(plugin.id, "plugin-001");
819        assert_eq!(plugin.name, "response-graphql");
820        assert!(plugin.healthy);
821        assert_eq!(plugin.types.len(), 2);
822    }
823
824    #[test]
825    fn deserialize_config_state() {
826        let json = r#"{
827            "latency": {
828                "enabled": true,
829                "base_ms": 100,
830                "jitter_ms": 20,
831                "tag_overrides": {"fast": 10}
832            },
833            "faults": {
834                "enabled": false,
835                "failure_rate": 0.05,
836                "status_codes": [500, 503]
837            },
838            "proxy": {
839                "enabled": false,
840                "upstream_url": null,
841                "timeout_seconds": 30
842            },
843            "traffic_shaping": {
844                "enabled": false,
845                "bandwidth": {},
846                "burst_loss": {}
847            },
848            "validation": {
849                "mode": "strict",
850                "aggregate_errors": true,
851                "validate_responses": false,
852                "overrides": {}
853            }
854        }"#;
855        let config: ConfigState = serde_json::from_str(json).unwrap();
856        assert!(config.latency.enabled);
857        assert_eq!(config.latency.base_ms, 100);
858        assert_eq!(config.latency.jitter_ms, 20);
859        assert!(!config.faults.enabled);
860        assert_eq!(config.faults.status_codes, vec![500, 503]);
861        assert!(!config.proxy.enabled);
862        assert_eq!(config.validation.mode, "strict");
863    }
864
865    #[test]
866    fn deserialize_fixture_info() {
867        let json = r#"{
868            "id": "fix-001",
869            "protocol": "http",
870            "method": "GET",
871            "path": "/api/users",
872            "saved_at": "2025-06-15T10:00:00Z",
873            "file_size": 2048,
874            "file_path": "/fixtures/users.json",
875            "fingerprint": "abc123def",
876            "metadata": {"tag": "v1"}
877        }"#;
878        let fixture: FixtureInfo = serde_json::from_str(json).unwrap();
879        assert_eq!(fixture.id, "fix-001");
880        assert_eq!(fixture.protocol, "http");
881        assert_eq!(fixture.file_size, 2048);
882    }
883
884    #[test]
885    fn deserialize_smoke_test_result() {
886        let json = r#"{
887            "id": "smoke-001",
888            "name": "Health endpoint",
889            "method": "GET",
890            "path": "/health",
891            "description": "Verify health endpoint returns 200",
892            "last_run": "2025-06-15T12:00:00Z",
893            "status": "passed",
894            "response_time_ms": 15,
895            "status_code": 200
896        }"#;
897        let result: SmokeTestResult = serde_json::from_str(json).unwrap();
898        assert_eq!(result.id, "smoke-001");
899        assert_eq!(result.status, "passed");
900        assert_eq!(result.status_code.unwrap(), 200);
901    }
902
903    #[test]
904    fn deserialize_workspace_info() {
905        let json = r#"{
906            "id": "ws-001",
907            "name": "default",
908            "description": "Default workspace",
909            "active": true,
910            "created_at": "2025-01-01T00:00:00Z",
911            "environments": ["dev", "staging"]
912        }"#;
913        let ws: WorkspaceInfo = serde_json::from_str(json).unwrap();
914        assert_eq!(ws.id, "ws-001");
915        assert!(ws.active);
916        assert_eq!(ws.environments, vec!["dev", "staging"]);
917    }
918
919    #[test]
920    fn deserialize_chaos_status() {
921        let json = r#"{
922            "enabled": true,
923            "active_scenario": "network-partition",
924            "settings": {"probability": 0.1}
925        }"#;
926        let chaos: ChaosStatus = serde_json::from_str(json).unwrap();
927        assert!(chaos.enabled);
928        assert_eq!(chaos.active_scenario.unwrap(), "network-partition");
929    }
930
931    #[test]
932    fn deserialize_time_travel_status() {
933        let json = r#"{
934            "enabled": true,
935            "current_time": "2025-01-01T00:00:00Z",
936            "scale_factor": 2.0,
937            "scheduled_responses": 5
938        }"#;
939        let tt: TimeTravelStatus = serde_json::from_str(json).unwrap();
940        assert!(tt.enabled);
941        assert!((tt.scale_factor.unwrap() - 2.0).abs() < f64::EPSILON);
942        assert_eq!(tt.scheduled_responses, 5);
943    }
944
945    #[test]
946    fn deserialize_time_travel_status_with_alias() {
947        // Server also sends time_scale (aliased to scale_factor)
948        let json = r#"{
949            "enabled": false,
950            "time_scale": 1.5
951        }"#;
952        let tt: TimeTravelStatus = serde_json::from_str(json).unwrap();
953        assert!(!tt.enabled);
954        assert!((tt.scale_factor.unwrap() - 1.5).abs() < f64::EPSILON);
955    }
956
957    #[test]
958    fn deserialize_chain_info() {
959        let json = r#"{
960            "id": "chain-001",
961            "name": "User flow",
962            "steps": [{"action": "create"}, {"action": "read"}],
963            "description": "End-to-end user CRUD"
964        }"#;
965        let chain: ChainInfo = serde_json::from_str(json).unwrap();
966        assert_eq!(chain.id, "chain-001");
967        assert_eq!(chain.steps.len(), 2);
968    }
969
970    #[test]
971    fn deserialize_audit_entry() {
972        let json = r#"{
973            "id": "audit-001",
974            "timestamp": "2025-06-15T12:00:00Z",
975            "action": "config.update",
976            "user": "admin",
977            "details": {"field": "latency_ms", "old": 50, "new": 100}
978        }"#;
979        let entry: AuditEntry = serde_json::from_str(json).unwrap();
980        assert_eq!(entry.id, "audit-001");
981        assert_eq!(entry.action, "config.update");
982        assert_eq!(entry.user, "admin");
983    }
984
985    #[test]
986    fn deserialize_analytics_summary() {
987        // Match actual server response format
988        let json = r#"{
989            "request_rate": 42.5,
990            "p95_latency_ms": 15.3,
991            "error_rate_percent": 0.5,
992            "active_connections": 12.0
993        }"#;
994        let summary: AnalyticsSummary = serde_json::from_str(json).unwrap();
995        assert!((summary.request_rate - 42.5).abs() < f64::EPSILON);
996        assert!((summary.error_rate_percent - 0.5).abs() < f64::EPSILON);
997    }
998
999    #[test]
1000    fn deserialize_analytics_summary_with_aliases() {
1001        // Also accept legacy field names
1002        let json = r#"{
1003            "total_requests": 10000,
1004            "unique_endpoints": 25,
1005            "error_rate": 0.02,
1006            "avg_response_time": 45.5,
1007            "top_endpoints": [
1008                {"endpoint": "/api/users", "count": 5000, "avg_time": 30.0}
1009            ]
1010        }"#;
1011        let summary: AnalyticsSummary = serde_json::from_str(json).unwrap();
1012        assert!((summary.request_rate - 10000.0).abs() < f64::EPSILON);
1013        assert_eq!(summary.unique_endpoints, 25);
1014        assert_eq!(summary.top_endpoints.len(), 1);
1015    }
1016
1017    #[test]
1018    fn deserialize_recorder_status() {
1019        let json = r#"{
1020            "recording": true,
1021            "recorded_count": 42
1022        }"#;
1023        let recorder: RecorderStatus = serde_json::from_str(json).unwrap();
1024        assert!(recorder.recording);
1025        assert_eq!(recorder.recorded_count, 42);
1026    }
1027
1028    #[test]
1029    fn deserialize_verification_result() {
1030        let json = r#"{
1031            "matched": true,
1032            "count": 3,
1033            "details": {"methods": ["GET", "POST"]}
1034        }"#;
1035        let result: VerificationResult = serde_json::from_str(json).unwrap();
1036        assert!(result.matched);
1037        assert_eq!(result.count, 3);
1038    }
1039
1040    #[test]
1041    fn deserialize_world_state_entry() {
1042        let json = r#"{
1043            "key": "user.count",
1044            "value": 42,
1045            "updated_at": "2025-06-15T12:00:00Z"
1046        }"#;
1047        let entry: WorldStateEntry = serde_json::from_str(json).unwrap();
1048        assert_eq!(entry.key, "user.count");
1049        assert_eq!(entry.value, serde_json::json!(42));
1050    }
1051
1052    #[test]
1053    fn deserialize_federation_peer() {
1054        let json = r#"{
1055            "id": "peer-001",
1056            "url": "http://peer1:9080",
1057            "status": "connected",
1058            "last_sync": "2025-06-15T12:00:00Z"
1059        }"#;
1060        let peer: FederationPeer = serde_json::from_str(json).unwrap();
1061        assert_eq!(peer.id, "peer-001");
1062        assert_eq!(peer.url, "http://peer1:9080");
1063        assert_eq!(peer.status, "connected");
1064    }
1065
1066    #[test]
1067    fn deserialize_contract_diff_capture() {
1068        let json = r#"{
1069            "id": "diff-001",
1070            "path": "/api/users",
1071            "method": "GET",
1072            "diff_status": "changed",
1073            "captured_at": "2025-06-15T12:00:00Z"
1074        }"#;
1075        let capture: ContractDiffCapture = serde_json::from_str(json).unwrap();
1076        assert_eq!(capture.id, "diff-001");
1077        assert_eq!(capture.diff_status, "changed");
1078    }
1079
1080    #[test]
1081    fn deserialize_metrics_data() {
1082        let json = r#"{
1083            "requests_by_endpoint": {"/api/users": 100, "/api/orders": 50},
1084            "response_time_percentiles": {"p50": 20, "p99": 200},
1085            "error_rate_by_endpoint": {"/api/users": 0.01}
1086        }"#;
1087        let metrics: MetricsData = serde_json::from_str(json).unwrap();
1088        assert_eq!(metrics.requests_by_endpoint.len(), 2);
1089        assert_eq!(*metrics.requests_by_endpoint.get("/api/users").unwrap(), 100);
1090        assert_eq!(metrics.response_time_percentiles.len(), 2);
1091    }
1092
1093    #[test]
1094    fn deserialize_system_info() {
1095        let json = r#"{
1096            "version": "0.3.31",
1097            "uptime_seconds": 3600,
1098            "memory_usage_mb": 128,
1099            "cpu_usage_percent": 15.5,
1100            "active_threads": 8,
1101            "total_routes": 42,
1102            "total_fixtures": 10
1103        }"#;
1104        let sys: SystemInfo = serde_json::from_str(json).unwrap();
1105        assert_eq!(sys.version, "0.3.31");
1106        assert_eq!(sys.uptime_seconds, 3600);
1107        assert_eq!(sys.total_routes, 42);
1108    }
1109
1110    #[test]
1111    fn deserialize_health_probe() {
1112        let json = r#"{
1113            "status": "ok",
1114            "checks": {"db": true, "redis": "connected"}
1115        }"#;
1116        let probe: HealthProbe = serde_json::from_str(json).unwrap();
1117        assert_eq!(probe.status, "ok");
1118        assert_eq!(probe.checks.len(), 2);
1119    }
1120
1121    #[test]
1122    fn roundtrip_serialize_deserialize() {
1123        let original = RecorderStatus {
1124            recording: true,
1125            recorded_count: 99,
1126        };
1127        let json = serde_json::to_string(&original).unwrap();
1128        let deserialized: RecorderStatus = serde_json::from_str(&json).unwrap();
1129        assert_eq!(deserialized.recording, original.recording);
1130        assert_eq!(deserialized.recorded_count, original.recorded_count);
1131    }
1132
1133    #[test]
1134    fn api_response_with_complex_data() {
1135        let json = r#"{
1136            "success": true,
1137            "data": [
1138                {"method": "GET", "path": "/users/{id}", "priority": 1, "has_fixtures": false, "request_count": 0, "error_count": 0},
1139                {"method": "POST", "path": "/users", "priority": 2, "has_fixtures": true, "request_count": 5, "error_count": 1}
1140            ],
1141            "error": null
1142        }"#;
1143        let resp: ApiResponse<Vec<RouteInfo>> = serde_json::from_str(json).unwrap();
1144        assert!(resp.success);
1145        let routes = resp.data.unwrap();
1146        assert_eq!(routes.len(), 2);
1147        assert_eq!(routes[0].path, "/users/{id}");
1148        assert_eq!(routes[1].request_count, 5);
1149    }
1150}