Skip to main content

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) = {
156                use base64::Engine;
157                base64::engine::general_purpose::STANDARD.decode(exact_bytes_b64)
158            } {
159                return data == expected.as_slice();
160            }
161        }
162
163        // Match by hex pattern
164        if let Some(ref hex_pattern) = criteria.data_pattern {
165            if let Ok(expected) = hex::decode(hex_pattern) {
166                return data == expected.as_slice();
167            }
168        }
169
170        // Match by text pattern (regex)
171        if let Some(ref text_pattern) = criteria.text_pattern {
172            if let Ok(re) = regex::Regex::new(text_pattern) {
173                if let Ok(text) = String::from_utf8(data.to_vec()) {
174                    return re.is_match(&text);
175                }
176            }
177        }
178
179        false
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::fixtures::{BehaviorConfig, MatchCriteria, TcpResponse};
187
188    fn create_test_fixture(id: &str, match_all: bool) -> TcpFixture {
189        TcpFixture {
190            identifier: id.to_string(),
191            name: format!("Fixture {}", id),
192            description: "Test fixture".to_string(),
193            match_criteria: MatchCriteria {
194                match_all,
195                ..Default::default()
196            },
197            response: TcpResponse {
198                data: "response".to_string(),
199                encoding: "text".to_string(),
200                file_path: None,
201                delay_ms: 0,
202                close_after_response: false,
203                keep_alive: true,
204            },
205            behavior: BehaviorConfig::default(),
206        }
207    }
208
209    #[test]
210    fn test_registry_new() {
211        let registry = TcpSpecRegistry::new();
212        assert!(registry.get_all_fixtures().is_empty());
213    }
214
215    #[test]
216    fn test_registry_default() {
217        let registry = TcpSpecRegistry::default();
218        assert!(registry.get_all_fixtures().is_empty());
219    }
220
221    #[test]
222    fn test_registry_add_fixture() {
223        let mut registry = TcpSpecRegistry::new();
224        let fixture = create_test_fixture("test-1", true);
225
226        registry.add_fixture(fixture);
227
228        assert_eq!(registry.get_all_fixtures().len(), 1);
229    }
230
231    #[test]
232    fn test_registry_get_fixture() {
233        let mut registry = TcpSpecRegistry::new();
234        let fixture = create_test_fixture("test-1", true);
235
236        registry.add_fixture(fixture);
237
238        let retrieved = registry.get_fixture("test-1");
239        assert!(retrieved.is_some());
240        assert_eq!(retrieved.unwrap().identifier, "test-1");
241    }
242
243    #[test]
244    fn test_registry_get_fixture_not_found() {
245        let registry = TcpSpecRegistry::new();
246        assert!(registry.get_fixture("nonexistent").is_none());
247    }
248
249    #[test]
250    fn test_registry_remove_fixture() {
251        let mut registry = TcpSpecRegistry::new();
252        let fixture = create_test_fixture("test-1", true);
253
254        registry.add_fixture(fixture);
255        let removed = registry.remove_fixture("test-1");
256
257        assert!(removed.is_some());
258        assert!(registry.get_fixture("test-1").is_none());
259    }
260
261    #[test]
262    fn test_registry_remove_fixture_not_found() {
263        let mut registry = TcpSpecRegistry::new();
264        let removed = registry.remove_fixture("nonexistent");
265        assert!(removed.is_none());
266    }
267
268    #[test]
269    fn test_registry_clear() {
270        let mut registry = TcpSpecRegistry::new();
271        registry.add_fixture(create_test_fixture("test-1", true));
272        registry.add_fixture(create_test_fixture("test-2", true));
273
274        registry.clear();
275
276        assert!(registry.get_all_fixtures().is_empty());
277    }
278
279    #[test]
280    fn test_registry_clone() {
281        let mut registry = TcpSpecRegistry::new();
282        registry.add_fixture(create_test_fixture("test-1", true));
283
284        let cloned = registry.clone();
285        assert_eq!(cloned.get_all_fixtures().len(), 1);
286    }
287
288    #[test]
289    fn test_registry_debug() {
290        let registry = TcpSpecRegistry::new();
291        let debug = format!("{:?}", registry);
292        assert!(debug.contains("TcpSpecRegistry"));
293    }
294
295    #[test]
296    fn test_fixture_matches_match_all() {
297        let fixture = create_test_fixture("test", true);
298        assert!(fixture.matches(b"any data"));
299        assert!(fixture.matches(b""));
300        assert!(fixture.matches(b"Hello World"));
301    }
302
303    #[test]
304    fn test_fixture_matches_min_length() {
305        let fixture = TcpFixture {
306            identifier: "test".to_string(),
307            name: "Test".to_string(),
308            description: String::new(),
309            match_criteria: MatchCriteria {
310                min_length: Some(5),
311                match_all: true,
312                ..Default::default()
313            },
314            response: TcpResponse {
315                data: "ok".to_string(),
316                encoding: "text".to_string(),
317                file_path: None,
318                delay_ms: 0,
319                close_after_response: false,
320                keep_alive: true,
321            },
322            behavior: BehaviorConfig::default(),
323        };
324
325        assert!(!fixture.matches(b"1234"));
326        assert!(fixture.matches(b"12345"));
327        assert!(fixture.matches(b"123456"));
328    }
329
330    #[test]
331    fn test_fixture_matches_max_length() {
332        let fixture = TcpFixture {
333            identifier: "test".to_string(),
334            name: "Test".to_string(),
335            description: String::new(),
336            match_criteria: MatchCriteria {
337                max_length: Some(5),
338                match_all: true,
339                ..Default::default()
340            },
341            response: TcpResponse {
342                data: "ok".to_string(),
343                encoding: "text".to_string(),
344                file_path: None,
345                delay_ms: 0,
346                close_after_response: false,
347                keep_alive: true,
348            },
349            behavior: BehaviorConfig::default(),
350        };
351
352        assert!(fixture.matches(b"12345"));
353        assert!(!fixture.matches(b"123456"));
354    }
355
356    #[test]
357    fn test_fixture_matches_text_pattern() {
358        let fixture = TcpFixture {
359            identifier: "test".to_string(),
360            name: "Test".to_string(),
361            description: String::new(),
362            match_criteria: MatchCriteria {
363                text_pattern: Some("hello.*world".to_string()),
364                ..Default::default()
365            },
366            response: TcpResponse {
367                data: "ok".to_string(),
368                encoding: "text".to_string(),
369                file_path: None,
370                delay_ms: 0,
371                close_after_response: false,
372                keep_alive: true,
373            },
374            behavior: BehaviorConfig::default(),
375        };
376
377        assert!(fixture.matches(b"hello world"));
378        assert!(fixture.matches(b"hello beautiful world"));
379        assert!(!fixture.matches(b"goodbye world"));
380    }
381
382    #[test]
383    fn test_fixture_matches_hex_pattern() {
384        let fixture = TcpFixture {
385            identifier: "test".to_string(),
386            name: "Test".to_string(),
387            description: String::new(),
388            match_criteria: MatchCriteria {
389                data_pattern: Some("48656c6c6f".to_string()), // "Hello" in hex
390                ..Default::default()
391            },
392            response: TcpResponse {
393                data: "ok".to_string(),
394                encoding: "text".to_string(),
395                file_path: None,
396                delay_ms: 0,
397                close_after_response: false,
398                keep_alive: true,
399            },
400            behavior: BehaviorConfig::default(),
401        };
402
403        assert!(fixture.matches(b"Hello"));
404        assert!(!fixture.matches(b"hello"));
405        assert!(!fixture.matches(b"World"));
406    }
407
408    #[test]
409    fn test_fixture_matches_exact_bytes() {
410        let fixture = TcpFixture {
411            identifier: "test".to_string(),
412            name: "Test".to_string(),
413            description: String::new(),
414            match_criteria: MatchCriteria {
415                exact_bytes: Some("SGVsbG8=".to_string()), // "Hello" in base64
416                ..Default::default()
417            },
418            response: TcpResponse {
419                data: "ok".to_string(),
420                encoding: "text".to_string(),
421                file_path: None,
422                delay_ms: 0,
423                close_after_response: false,
424                keep_alive: true,
425            },
426            behavior: BehaviorConfig::default(),
427        };
428
429        assert!(fixture.matches(b"Hello"));
430        assert!(!fixture.matches(b"hello"));
431    }
432
433    #[test]
434    fn test_fixture_no_match() {
435        let fixture = TcpFixture {
436            identifier: "test".to_string(),
437            name: "Test".to_string(),
438            description: String::new(),
439            match_criteria: MatchCriteria::default(), // No criteria, match_all is false
440            response: TcpResponse {
441                data: "ok".to_string(),
442                encoding: "text".to_string(),
443                file_path: None,
444                delay_ms: 0,
445                close_after_response: false,
446                keep_alive: true,
447            },
448            behavior: BehaviorConfig::default(),
449        };
450
451        // With no matching criteria and match_all=false, should not match
452        assert!(!fixture.matches(b"anything"));
453    }
454
455    #[test]
456    fn test_find_matching_fixture() {
457        let mut registry = TcpSpecRegistry::new();
458
459        // Add a fixture that matches all
460        registry.add_fixture(create_test_fixture("catch-all", true));
461
462        let matched = registry.find_matching_fixture(b"test data");
463        assert!(matched.is_some());
464        assert_eq!(matched.unwrap().identifier, "catch-all");
465    }
466
467    #[test]
468    fn test_find_matching_fixture_none() {
469        let mut registry = TcpSpecRegistry::new();
470
471        // Add a fixture that doesn't match
472        registry.add_fixture(create_test_fixture("no-match", false));
473
474        let matched = registry.find_matching_fixture(b"test data");
475        assert!(matched.is_none());
476    }
477}