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