Skip to main content

mockforge_tcp/
fixtures.rs

1//! TCP fixture definitions and loading
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// A TCP fixture defining how to handle TCP connections
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct TcpFixture {
9    /// Unique identifier for this fixture
10    pub identifier: String,
11
12    /// Human-readable name
13    pub name: String,
14
15    /// Description of what this fixture does
16    #[serde(default)]
17    pub description: String,
18
19    /// Matching criteria for incoming data
20    pub match_criteria: MatchCriteria,
21
22    /// Response configuration
23    pub response: TcpResponse,
24
25    /// Behavior simulation
26    #[serde(default)]
27    pub behavior: BehaviorConfig,
28}
29
30/// Criteria for matching incoming TCP data
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct MatchCriteria {
33    /// Match by data pattern (hex string, e.g., "48656c6c6f" for "Hello")
34    #[serde(default)]
35    pub data_pattern: Option<String>,
36
37    /// Match by data pattern (regex for text data)
38    #[serde(default)]
39    pub text_pattern: Option<String>,
40
41    /// Match by exact bytes (base64 encoded)
42    #[serde(default)]
43    pub exact_bytes: Option<String>,
44
45    /// Match all (default fixture)
46    #[serde(default)]
47    pub match_all: bool,
48
49    /// Minimum data length required to match
50    #[serde(default)]
51    pub min_length: Option<usize>,
52
53    /// Maximum data length to match
54    #[serde(default)]
55    pub max_length: Option<usize>,
56}
57
58/// TCP response configuration
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct TcpResponse {
61    /// Response data (hex string, base64, or plain text)
62    pub data: String,
63
64    /// Data encoding: "hex", "base64", "text", "file"
65    #[serde(default = "default_encoding")]
66    pub encoding: String,
67
68    /// File path to load response from (if encoding is "file")
69    #[serde(default)]
70    pub file_path: Option<PathBuf>,
71
72    /// Delay before responding (milliseconds)
73    #[serde(default)]
74    pub delay_ms: u64,
75
76    /// Close connection after sending response
77    #[serde(default)]
78    pub close_after_response: bool,
79
80    /// Keep connection open for streaming
81    #[serde(default)]
82    pub keep_alive: bool,
83}
84
85fn default_encoding() -> String {
86    "text".to_string()
87}
88
89/// Behavior simulation configuration
90#[derive(Debug, Clone, Serialize, Deserialize, Default)]
91pub struct BehaviorConfig {
92    /// Simulate connection delay (milliseconds)
93    #[serde(default)]
94    pub connection_delay_ms: u64,
95
96    /// Simulate slow data transfer (bytes per second)
97    #[serde(default)]
98    pub throttle_bytes_per_sec: Option<u64>,
99
100    /// Simulate connection drops (probability 0.0-1.0)
101    #[serde(default)]
102    pub drop_connection_probability: f64,
103
104    /// Simulate partial data (send N bytes then close)
105    #[serde(default)]
106    pub partial_data_bytes: Option<usize>,
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    fn create_test_fixture() -> TcpFixture {
114        TcpFixture {
115            identifier: "test-fixture".to_string(),
116            name: "Test Fixture".to_string(),
117            description: "A test fixture".to_string(),
118            match_criteria: MatchCriteria::default(),
119            response: TcpResponse {
120                data: "Hello".to_string(),
121                encoding: "text".to_string(),
122                file_path: None,
123                delay_ms: 0,
124                close_after_response: false,
125                keep_alive: true,
126            },
127            behavior: BehaviorConfig::default(),
128        }
129    }
130
131    #[test]
132    fn test_match_criteria_default() {
133        let criteria = MatchCriteria::default();
134        assert!(criteria.data_pattern.is_none());
135        assert!(criteria.text_pattern.is_none());
136        assert!(criteria.exact_bytes.is_none());
137        assert!(!criteria.match_all);
138        assert!(criteria.min_length.is_none());
139        assert!(criteria.max_length.is_none());
140    }
141
142    #[test]
143    fn test_match_criteria_clone() {
144        let criteria = MatchCriteria {
145            data_pattern: Some("48656c6c6f".to_string()),
146            text_pattern: Some("hello.*".to_string()),
147            exact_bytes: None,
148            match_all: true,
149            min_length: Some(5),
150            max_length: Some(100),
151        };
152
153        let cloned = criteria.clone();
154        assert_eq!(criteria.data_pattern, cloned.data_pattern);
155        assert_eq!(criteria.text_pattern, cloned.text_pattern);
156        assert_eq!(criteria.match_all, cloned.match_all);
157    }
158
159    #[test]
160    fn test_match_criteria_debug() {
161        let criteria = MatchCriteria::default();
162        let debug = format!("{:?}", criteria);
163        assert!(debug.contains("MatchCriteria"));
164    }
165
166    #[test]
167    fn test_tcp_response_default_encoding() {
168        let json = r#"{
169            "data": "test"
170        }"#;
171        let response: TcpResponse = serde_json::from_str(json).unwrap();
172        assert_eq!(response.encoding, "text");
173    }
174
175    #[test]
176    fn test_tcp_response_clone() {
177        let response = TcpResponse {
178            data: "Hello World".to_string(),
179            encoding: "base64".to_string(),
180            file_path: Some(PathBuf::from("/path/to/file")),
181            delay_ms: 100,
182            close_after_response: true,
183            keep_alive: false,
184        };
185
186        let cloned = response.clone();
187        assert_eq!(response.data, cloned.data);
188        assert_eq!(response.encoding, cloned.encoding);
189        assert_eq!(response.file_path, cloned.file_path);
190    }
191
192    #[test]
193    fn test_tcp_response_debug() {
194        let response = TcpResponse {
195            data: "test".to_string(),
196            encoding: "text".to_string(),
197            file_path: None,
198            delay_ms: 0,
199            close_after_response: false,
200            keep_alive: true,
201        };
202        let debug = format!("{:?}", response);
203        assert!(debug.contains("TcpResponse"));
204    }
205
206    #[test]
207    fn test_behavior_config_default() {
208        let config = BehaviorConfig::default();
209        assert_eq!(config.connection_delay_ms, 0);
210        assert!(config.throttle_bytes_per_sec.is_none());
211        assert_eq!(config.drop_connection_probability, 0.0);
212        assert!(config.partial_data_bytes.is_none());
213    }
214
215    #[test]
216    fn test_behavior_config_clone() {
217        let config = BehaviorConfig {
218            connection_delay_ms: 500,
219            throttle_bytes_per_sec: Some(1024),
220            drop_connection_probability: 0.1,
221            partial_data_bytes: Some(100),
222        };
223
224        let cloned = config.clone();
225        assert_eq!(config.connection_delay_ms, cloned.connection_delay_ms);
226        assert_eq!(config.throttle_bytes_per_sec, cloned.throttle_bytes_per_sec);
227        assert_eq!(config.drop_connection_probability, cloned.drop_connection_probability);
228    }
229
230    #[test]
231    fn test_behavior_config_debug() {
232        let config = BehaviorConfig::default();
233        let debug = format!("{:?}", config);
234        assert!(debug.contains("BehaviorConfig"));
235    }
236
237    #[test]
238    fn test_tcp_fixture_clone() {
239        let fixture = create_test_fixture();
240        let cloned = fixture.clone();
241        assert_eq!(fixture.identifier, cloned.identifier);
242        assert_eq!(fixture.name, cloned.name);
243    }
244
245    #[test]
246    fn test_tcp_fixture_debug() {
247        let fixture = create_test_fixture();
248        let debug = format!("{:?}", fixture);
249        assert!(debug.contains("TcpFixture"));
250        assert!(debug.contains("test-fixture"));
251    }
252
253    #[test]
254    fn test_tcp_fixture_serialize() {
255        let fixture = create_test_fixture();
256        let json = serde_json::to_string(&fixture).unwrap();
257        assert!(json.contains("\"identifier\":\"test-fixture\""));
258        assert!(json.contains("\"name\":\"Test Fixture\""));
259    }
260
261    #[test]
262    fn test_tcp_fixture_deserialize() {
263        let yaml = r#"
264identifier: my-fixture
265name: My Fixture
266description: A fixture for testing
267match_criteria:
268  match_all: true
269response:
270  data: "Hello"
271  encoding: text
272  delay_ms: 0
273  close_after_response: false
274  keep_alive: true
275"#;
276
277        let fixture: TcpFixture = serde_yaml::from_str(yaml).unwrap();
278        assert_eq!(fixture.identifier, "my-fixture");
279        assert_eq!(fixture.name, "My Fixture");
280        assert!(fixture.match_criteria.match_all);
281    }
282
283    #[test]
284    fn test_tcp_response_with_delay() {
285        let response = TcpResponse {
286            data: "delayed".to_string(),
287            encoding: "text".to_string(),
288            file_path: None,
289            delay_ms: 1000,
290            close_after_response: true,
291            keep_alive: false,
292        };
293
294        assert_eq!(response.delay_ms, 1000);
295        assert!(response.close_after_response);
296        assert!(!response.keep_alive);
297    }
298
299    #[test]
300    fn test_match_criteria_with_lengths() {
301        let criteria = MatchCriteria {
302            min_length: Some(10),
303            max_length: Some(1000),
304            ..Default::default()
305        };
306
307        assert_eq!(criteria.min_length, Some(10));
308        assert_eq!(criteria.max_length, Some(1000));
309    }
310}