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) = {
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 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 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()), ..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()), ..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(), 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 assert!(!fixture.matches(b"anything"));
453 }
454
455 #[test]
456 fn test_find_matching_fixture() {
457 let mut registry = TcpSpecRegistry::new();
458
459 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 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}