mockforge_tcp/
spec_registry.rs

1//! TCP spec registry for managing TCP fixtures
2
3use crate::fixtures::TcpFixture;
4use mockforge_core::Result;
5use std::collections::HashMap;
6use std::path::Path;
7use tracing::{debug, info, warn};
8
9/// Registry for TCP fixtures
10#[derive(Debug, Clone)]
11pub struct TcpSpecRegistry {
12    fixtures: HashMap<String, TcpFixture>,
13}
14
15impl TcpSpecRegistry {
16    /// Create a new empty registry
17    pub fn new() -> Self {
18        Self {
19            fixtures: HashMap::new(),
20        }
21    }
22
23    /// Load fixtures from a directory
24    pub fn load_fixtures<P: AsRef<Path>>(&mut self, fixtures_dir: P) -> Result<()> {
25        let fixtures_dir = fixtures_dir.as_ref();
26        if !fixtures_dir.exists() {
27            debug!("TCP fixtures directory does not exist: {:?}", fixtures_dir);
28            return Ok(());
29        }
30
31        info!("Loading TCP fixtures from {:?}", fixtures_dir);
32
33        let entries = std::fs::read_dir(fixtures_dir).map_err(|e| {
34            mockforge_core::Error::generic(format!("Failed to read fixtures directory: {}", e))
35        })?;
36
37        let mut loaded_count = 0;
38
39        for entry in entries {
40            let entry = entry.map_err(|e| {
41                mockforge_core::Error::generic(format!("Failed to read directory entry: {}", e))
42            })?;
43            let path = entry.path();
44
45            if path.is_file() {
46                match path.extension().and_then(|s| s.to_str()) {
47                    Some("yaml") | Some("yml") | Some("json") => {
48                        if let Err(e) = self.load_fixture_file(&path) {
49                            warn!("Failed to load fixture from {:?}: {}", path, e);
50                        } else {
51                            loaded_count += 1;
52                        }
53                    }
54                    _ => {
55                        debug!("Skipping non-fixture file: {:?}", path);
56                    }
57                }
58            }
59        }
60
61        info!("Loaded {} TCP fixture(s)", loaded_count);
62        Ok(())
63    }
64
65    /// Load a single fixture file
66    fn load_fixture_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
67        let path = path.as_ref();
68        let content = std::fs::read_to_string(path).map_err(|e| {
69            mockforge_core::Error::generic(format!("Failed to read fixture file: {}", e))
70        })?;
71
72        let fixtures: Vec<TcpFixture> = if path.extension().and_then(|s| s.to_str()) == Some("json")
73        {
74            serde_json::from_str(&content).map_err(|e| {
75                mockforge_core::Error::generic(format!("Failed to parse JSON fixture: {}", e))
76            })?
77        } else {
78            serde_yaml::from_str(&content).map_err(|e| {
79                mockforge_core::Error::generic(format!("Failed to parse YAML fixture: {}", e))
80            })?
81        };
82
83        for fixture in fixtures {
84            let identifier = fixture.identifier.clone();
85            self.fixtures.insert(identifier, fixture);
86        }
87
88        Ok(())
89    }
90
91    /// Add a fixture to the registry
92    pub fn add_fixture(&mut self, fixture: TcpFixture) {
93        let identifier = fixture.identifier.clone();
94        self.fixtures.insert(identifier, fixture);
95    }
96
97    /// Get a fixture by identifier
98    pub fn get_fixture(&self, identifier: &str) -> Option<&TcpFixture> {
99        self.fixtures.get(identifier)
100    }
101
102    /// Find a fixture matching the given data
103    pub fn find_matching_fixture(&self, data: &[u8]) -> Option<&TcpFixture> {
104        // Try to match against all fixtures
105        self.fixtures.values().find(|&fixture| fixture.matches(data)).map(|v| v as _)
106    }
107
108    /// Get all fixtures
109    pub fn get_all_fixtures(&self) -> Vec<&TcpFixture> {
110        self.fixtures.values().collect()
111    }
112
113    /// Remove a fixture
114    pub fn remove_fixture(&mut self, identifier: &str) -> Option<TcpFixture> {
115        self.fixtures.remove(identifier)
116    }
117
118    /// Clear all fixtures
119    pub fn clear(&mut self) {
120        self.fixtures.clear();
121    }
122}
123
124impl Default for TcpSpecRegistry {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl TcpFixture {
131    /// Check if this fixture matches the given data
132    pub fn matches(&self, data: &[u8]) -> bool {
133        let criteria = &self.match_criteria;
134
135        // Check length constraints
136        if let Some(min_len) = criteria.min_length {
137            if data.len() < min_len {
138                return false;
139            }
140        }
141
142        if let Some(max_len) = criteria.max_length {
143            if data.len() > max_len {
144                return false;
145            }
146        }
147
148        // Match all - always matches
149        if criteria.match_all {
150            return true;
151        }
152
153        // Match by exact bytes
154        if let Some(ref exact_bytes_b64) = criteria.exact_bytes {
155            if let Ok(expected) = base64::decode(exact_bytes_b64) {
156                return data == expected.as_slice();
157            }
158        }
159
160        // Match by hex pattern
161        if let Some(ref hex_pattern) = criteria.data_pattern {
162            if let Ok(expected) = hex::decode(hex_pattern) {
163                return data == expected.as_slice();
164            }
165        }
166
167        // Match by text pattern (regex)
168        if let Some(ref text_pattern) = criteria.text_pattern {
169            if let Ok(re) = regex::Regex::new(text_pattern) {
170                if let Ok(text) = String::from_utf8(data.to_vec()) {
171                    return re.is_match(&text);
172                }
173            }
174        }
175
176        false
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::fixtures::{BehaviorConfig, MatchCriteria, TcpResponse};
184
185    fn create_test_fixture(id: &str, match_all: bool) -> TcpFixture {
186        TcpFixture {
187            identifier: id.to_string(),
188            name: format!("Fixture {}", id),
189            description: "Test fixture".to_string(),
190            match_criteria: MatchCriteria {
191                match_all,
192                ..Default::default()
193            },
194            response: TcpResponse {
195                data: "response".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            behavior: BehaviorConfig::default(),
203        }
204    }
205
206    #[test]
207    fn test_registry_new() {
208        let registry = TcpSpecRegistry::new();
209        assert!(registry.get_all_fixtures().is_empty());
210    }
211
212    #[test]
213    fn test_registry_default() {
214        let registry = TcpSpecRegistry::default();
215        assert!(registry.get_all_fixtures().is_empty());
216    }
217
218    #[test]
219    fn test_registry_add_fixture() {
220        let mut registry = TcpSpecRegistry::new();
221        let fixture = create_test_fixture("test-1", true);
222
223        registry.add_fixture(fixture);
224
225        assert_eq!(registry.get_all_fixtures().len(), 1);
226    }
227
228    #[test]
229    fn test_registry_get_fixture() {
230        let mut registry = TcpSpecRegistry::new();
231        let fixture = create_test_fixture("test-1", true);
232
233        registry.add_fixture(fixture);
234
235        let retrieved = registry.get_fixture("test-1");
236        assert!(retrieved.is_some());
237        assert_eq!(retrieved.unwrap().identifier, "test-1");
238    }
239
240    #[test]
241    fn test_registry_get_fixture_not_found() {
242        let registry = TcpSpecRegistry::new();
243        assert!(registry.get_fixture("nonexistent").is_none());
244    }
245
246    #[test]
247    fn test_registry_remove_fixture() {
248        let mut registry = TcpSpecRegistry::new();
249        let fixture = create_test_fixture("test-1", true);
250
251        registry.add_fixture(fixture);
252        let removed = registry.remove_fixture("test-1");
253
254        assert!(removed.is_some());
255        assert!(registry.get_fixture("test-1").is_none());
256    }
257
258    #[test]
259    fn test_registry_remove_fixture_not_found() {
260        let mut registry = TcpSpecRegistry::new();
261        let removed = registry.remove_fixture("nonexistent");
262        assert!(removed.is_none());
263    }
264
265    #[test]
266    fn test_registry_clear() {
267        let mut registry = TcpSpecRegistry::new();
268        registry.add_fixture(create_test_fixture("test-1", true));
269        registry.add_fixture(create_test_fixture("test-2", true));
270
271        registry.clear();
272
273        assert!(registry.get_all_fixtures().is_empty());
274    }
275
276    #[test]
277    fn test_registry_clone() {
278        let mut registry = TcpSpecRegistry::new();
279        registry.add_fixture(create_test_fixture("test-1", true));
280
281        let cloned = registry.clone();
282        assert_eq!(cloned.get_all_fixtures().len(), 1);
283    }
284
285    #[test]
286    fn test_registry_debug() {
287        let registry = TcpSpecRegistry::new();
288        let debug = format!("{:?}", registry);
289        assert!(debug.contains("TcpSpecRegistry"));
290    }
291
292    #[test]
293    fn test_fixture_matches_match_all() {
294        let fixture = create_test_fixture("test", true);
295        assert!(fixture.matches(b"any data"));
296        assert!(fixture.matches(b""));
297        assert!(fixture.matches(b"Hello World"));
298    }
299
300    #[test]
301    fn test_fixture_matches_min_length() {
302        let fixture = TcpFixture {
303            identifier: "test".to_string(),
304            name: "Test".to_string(),
305            description: String::new(),
306            match_criteria: MatchCriteria {
307                min_length: Some(5),
308                match_all: true,
309                ..Default::default()
310            },
311            response: TcpResponse {
312                data: "ok".to_string(),
313                encoding: "text".to_string(),
314                file_path: None,
315                delay_ms: 0,
316                close_after_response: false,
317                keep_alive: true,
318            },
319            behavior: BehaviorConfig::default(),
320        };
321
322        assert!(!fixture.matches(b"1234"));
323        assert!(fixture.matches(b"12345"));
324        assert!(fixture.matches(b"123456"));
325    }
326
327    #[test]
328    fn test_fixture_matches_max_length() {
329        let fixture = TcpFixture {
330            identifier: "test".to_string(),
331            name: "Test".to_string(),
332            description: String::new(),
333            match_criteria: MatchCriteria {
334                max_length: Some(5),
335                match_all: true,
336                ..Default::default()
337            },
338            response: TcpResponse {
339                data: "ok".to_string(),
340                encoding: "text".to_string(),
341                file_path: None,
342                delay_ms: 0,
343                close_after_response: false,
344                keep_alive: true,
345            },
346            behavior: BehaviorConfig::default(),
347        };
348
349        assert!(fixture.matches(b"12345"));
350        assert!(!fixture.matches(b"123456"));
351    }
352
353    #[test]
354    fn test_fixture_matches_text_pattern() {
355        let fixture = TcpFixture {
356            identifier: "test".to_string(),
357            name: "Test".to_string(),
358            description: String::new(),
359            match_criteria: MatchCriteria {
360                text_pattern: Some("hello.*world".to_string()),
361                ..Default::default()
362            },
363            response: TcpResponse {
364                data: "ok".to_string(),
365                encoding: "text".to_string(),
366                file_path: None,
367                delay_ms: 0,
368                close_after_response: false,
369                keep_alive: true,
370            },
371            behavior: BehaviorConfig::default(),
372        };
373
374        assert!(fixture.matches(b"hello world"));
375        assert!(fixture.matches(b"hello beautiful world"));
376        assert!(!fixture.matches(b"goodbye world"));
377    }
378
379    #[test]
380    fn test_fixture_matches_hex_pattern() {
381        let fixture = TcpFixture {
382            identifier: "test".to_string(),
383            name: "Test".to_string(),
384            description: String::new(),
385            match_criteria: MatchCriteria {
386                data_pattern: Some("48656c6c6f".to_string()), // "Hello" in hex
387                ..Default::default()
388            },
389            response: TcpResponse {
390                data: "ok".to_string(),
391                encoding: "text".to_string(),
392                file_path: None,
393                delay_ms: 0,
394                close_after_response: false,
395                keep_alive: true,
396            },
397            behavior: BehaviorConfig::default(),
398        };
399
400        assert!(fixture.matches(b"Hello"));
401        assert!(!fixture.matches(b"hello"));
402        assert!(!fixture.matches(b"World"));
403    }
404
405    #[test]
406    fn test_fixture_matches_exact_bytes() {
407        let fixture = TcpFixture {
408            identifier: "test".to_string(),
409            name: "Test".to_string(),
410            description: String::new(),
411            match_criteria: MatchCriteria {
412                exact_bytes: Some("SGVsbG8=".to_string()), // "Hello" in base64
413                ..Default::default()
414            },
415            response: TcpResponse {
416                data: "ok".to_string(),
417                encoding: "text".to_string(),
418                file_path: None,
419                delay_ms: 0,
420                close_after_response: false,
421                keep_alive: true,
422            },
423            behavior: BehaviorConfig::default(),
424        };
425
426        assert!(fixture.matches(b"Hello"));
427        assert!(!fixture.matches(b"hello"));
428    }
429
430    #[test]
431    fn test_fixture_no_match() {
432        let fixture = TcpFixture {
433            identifier: "test".to_string(),
434            name: "Test".to_string(),
435            description: String::new(),
436            match_criteria: MatchCriteria::default(), // No criteria, match_all is false
437            response: TcpResponse {
438                data: "ok".to_string(),
439                encoding: "text".to_string(),
440                file_path: None,
441                delay_ms: 0,
442                close_after_response: false,
443                keep_alive: true,
444            },
445            behavior: BehaviorConfig::default(),
446        };
447
448        // With no matching criteria and match_all=false, should not match
449        assert!(!fixture.matches(b"anything"));
450    }
451
452    #[test]
453    fn test_find_matching_fixture() {
454        let mut registry = TcpSpecRegistry::new();
455
456        // Add a fixture that matches all
457        registry.add_fixture(create_test_fixture("catch-all", true));
458
459        let matched = registry.find_matching_fixture(b"test data");
460        assert!(matched.is_some());
461        assert_eq!(matched.unwrap().identifier, "catch-all");
462    }
463
464    #[test]
465    fn test_find_matching_fixture_none() {
466        let mut registry = TcpSpecRegistry::new();
467
468        // Add a fixture that doesn't match
469        registry.add_fixture(create_test_fixture("no-match", false));
470
471        let matched = registry.find_matching_fixture(b"test data");
472        assert!(matched.is_none());
473    }
474}