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