1use crate::fixtures::TcpFixture;
4use mockforge_core::Result;
5use std::collections::HashMap;
6use std::path::Path;
7use tracing::{debug, info, warn};
8
9#[derive(Debug, Clone)]
11pub struct TcpSpecRegistry {
12 fixtures: HashMap<String, TcpFixture>,
13}
14
15impl TcpSpecRegistry {
16 pub fn new() -> Self {
18 Self {
19 fixtures: HashMap::new(),
20 }
21 }
22
23 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 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 pub fn add_fixture(&mut self, fixture: TcpFixture) {
93 let identifier = fixture.identifier.clone();
94 self.fixtures.insert(identifier, fixture);
95 }
96
97 pub fn get_fixture(&self, identifier: &str) -> Option<&TcpFixture> {
99 self.fixtures.get(identifier)
100 }
101
102 pub fn find_matching_fixture(&self, data: &[u8]) -> Option<&TcpFixture> {
104 self.fixtures.values().find(|&fixture| fixture.matches(data)).map(|v| v as _)
106 }
107
108 pub fn get_all_fixtures(&self) -> Vec<&TcpFixture> {
110 self.fixtures.values().collect()
111 }
112
113 pub fn remove_fixture(&mut self, identifier: &str) -> Option<TcpFixture> {
115 self.fixtures.remove(identifier)
116 }
117
118 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 pub fn matches(&self, data: &[u8]) -> bool {
133 let criteria = &self.match_criteria;
134
135 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 if criteria.match_all {
150 return true;
151 }
152
153 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 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 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()), ..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()), ..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(), 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 assert!(!fixture.matches(b"anything"));
450 }
451
452 #[test]
453 fn test_find_matching_fixture() {
454 let mut registry = TcpSpecRegistry::new();
455
456 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 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}