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