mockforge_ftp/
spec_registry.rs

1use crate::fixtures::{FtpFixture, UploadRule};
2use crate::vfs::VirtualFileSystem;
3use mockforge_core::protocol_abstraction::{
4    Protocol, ProtocolRequest, ProtocolResponse, ResponseStatus, SpecOperation, SpecRegistry,
5    ValidationError, ValidationResult,
6};
7use mockforge_core::Result;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Tracked upload information
12#[derive(Debug, Clone)]
13pub struct UploadRecord {
14    pub id: String,
15    pub path: std::path::PathBuf,
16    pub size: u64,
17    pub uploaded_at: chrono::DateTime<chrono::Utc>,
18    pub rule_name: Option<String>,
19}
20
21/// FTP Spec Registry for MockForge
22#[derive(Debug, Clone)]
23pub struct FtpSpecRegistry {
24    pub fixtures: Vec<FtpFixture>,
25    pub vfs: Arc<VirtualFileSystem>,
26    pub uploads: Arc<std::sync::RwLock<Vec<UploadRecord>>>,
27}
28
29impl FtpSpecRegistry {
30    pub fn new() -> Self {
31        Self {
32            fixtures: Vec::new(),
33            vfs: Arc::new(VirtualFileSystem::new(std::path::PathBuf::from("/"))),
34            uploads: Arc::new(std::sync::RwLock::new(Vec::new())),
35        }
36    }
37
38    pub fn with_fixtures(mut self, fixtures: Vec<FtpFixture>) -> Result<Self> {
39        // Load virtual files into VFS fixtures
40        let mut vfs_fixtures = Vec::new();
41        for fixture in &fixtures {
42            for virtual_file in &fixture.virtual_files {
43                vfs_fixtures.push(virtual_file.clone().to_file_fixture());
44            }
45        }
46        self.vfs
47            .load_fixtures(vfs_fixtures)
48            .map_err(|e| mockforge_core::Error::from(e.to_string()))?;
49
50        self.fixtures = fixtures;
51        Ok(self)
52    }
53
54    pub fn with_vfs(mut self, vfs: Arc<VirtualFileSystem>) -> Self {
55        self.vfs = vfs;
56        self
57    }
58
59    pub fn find_upload_rule(&self, path: &str) -> Option<&UploadRule> {
60        for fixture in &self.fixtures {
61            for rule in &fixture.upload_rules {
62                if rule.matches_path(path) {
63                    return Some(rule);
64                }
65            }
66        }
67        None
68    }
69
70    pub fn record_upload(
71        &self,
72        path: std::path::PathBuf,
73        size: u64,
74        rule_name: Option<String>,
75    ) -> Result<String> {
76        let id = uuid::Uuid::new_v4().to_string();
77        let record = UploadRecord {
78            id: id.clone(),
79            path,
80            size,
81            uploaded_at: chrono::Utc::now(),
82            rule_name,
83        };
84
85        let mut uploads = self.uploads.write().unwrap();
86        uploads.push(record);
87
88        Ok(id)
89    }
90
91    pub fn get_uploads(&self) -> Vec<UploadRecord> {
92        self.uploads.read().unwrap().clone()
93    }
94
95    pub fn get_upload(&self, id: &str) -> Option<UploadRecord> {
96        self.uploads.read().unwrap().iter().find(|u| u.id == id).cloned()
97    }
98}
99
100impl Default for FtpSpecRegistry {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl SpecRegistry for FtpSpecRegistry {
107    fn protocol(&self) -> Protocol {
108        Protocol::Ftp
109    }
110
111    fn operations(&self) -> Vec<SpecOperation> {
112        self.fixtures
113            .iter()
114            .flat_map(|fixture| {
115                fixture.virtual_files.iter().map(|file| SpecOperation {
116                    name: format!("{}:{}", fixture.name, file.path.display()),
117                    path: file.path.to_string_lossy().to_string(),
118                    operation_type: "RETR".to_string(),
119                    input_schema: None,
120                    output_schema: None,
121                    metadata: HashMap::from([
122                        (
123                            "description".to_string(),
124                            fixture.description.clone().unwrap_or_default(),
125                        ),
126                        ("permissions".to_string(), file.permissions.clone()),
127                        ("owner".to_string(), file.owner.clone()),
128                    ]),
129                })
130            })
131            .collect()
132    }
133
134    fn find_operation(&self, operation: &str, path: &str) -> Option<SpecOperation> {
135        self.fixtures
136            .iter()
137            .flat_map(|fixture| &fixture.virtual_files)
138            .find(|file| file.path.to_string_lossy() == path)
139            .map(|file| SpecOperation {
140                name: path.to_string(),
141                path: path.to_string(),
142                operation_type: operation.to_string(),
143                input_schema: None,
144                output_schema: None,
145                metadata: HashMap::from([
146                    ("permissions".to_string(), file.permissions.clone()),
147                    ("owner".to_string(), file.owner.clone()),
148                    ("group".to_string(), file.group.clone()),
149                ]),
150            })
151    }
152
153    fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult> {
154        if request.protocol != Protocol::Ftp {
155            return Ok(ValidationResult::failure(vec![ValidationError {
156                message: "Invalid protocol for FTP registry".to_string(),
157                path: Some("protocol".to_string()),
158                code: Some("invalid_protocol".to_string()),
159            }]));
160        }
161
162        // Basic validation - operation should be a valid FTP command
163        let valid_operations = [
164            "RETR", "STOR", "LIST", "DELE", "MKD", "RMD", "CWD", "PWD", "SIZE", "MDTM",
165        ];
166        if !valid_operations.contains(&request.operation.as_str()) {
167            return Ok(ValidationResult::failure(vec![ValidationError {
168                message: format!("Unsupported FTP operation: {}", request.operation),
169                path: Some("operation".to_string()),
170                code: Some("unsupported_operation".to_string()),
171            }]));
172        }
173
174        Ok(ValidationResult::success())
175    }
176
177    fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse> {
178        match request.operation.as_str() {
179            "RETR" => {
180                // Download file
181                let path = std::path::Path::new(&request.path);
182                if let Some(file) = self.vfs.get_file(path) {
183                    let content = file
184                        .render_content()
185                        .map_err(|e| mockforge_core::Error::from(e.to_string()))?;
186                    Ok(ProtocolResponse {
187                        status: ResponseStatus::FtpStatus(150), // Opening data connection
188                        body: content,
189                        metadata: HashMap::from([
190                            ("size".to_string(), file.metadata.size.to_string()),
191                            ("path".to_string(), request.path.clone()),
192                        ]),
193                        content_type: "application/octet-stream".to_string(),
194                    })
195                } else {
196                    Ok(ProtocolResponse {
197                        status: ResponseStatus::FtpStatus(550), // File not found
198                        body: b"File not found".to_vec(),
199                        metadata: HashMap::new(),
200                        content_type: "text/plain".to_string(),
201                    })
202                }
203            }
204            "STOR" => {
205                // Upload file
206                let path = &request.path;
207                if let Some(rule) = self.find_upload_rule(path) {
208                    if let Some(body) = &request.body {
209                        // Validate upload
210                        if let Err(validation_error) = rule.validate_file(body, path) {
211                            return Ok(ProtocolResponse {
212                                status: ResponseStatus::FtpStatus(550), // Permission denied
213                                body: validation_error.into_bytes(),
214                                metadata: HashMap::new(),
215                                content_type: "text/plain".to_string(),
216                            });
217                        }
218
219                        if rule.auto_accept {
220                            Ok(ProtocolResponse {
221                                status: ResponseStatus::FtpStatus(226), // Transfer complete
222                                body: b"Transfer complete".to_vec(),
223                                metadata: HashMap::from([
224                                    ("path".to_string(), path.clone()),
225                                    ("size".to_string(), body.len().to_string()),
226                                ]),
227                                content_type: "text/plain".to_string(),
228                            })
229                        } else {
230                            Ok(ProtocolResponse {
231                                status: ResponseStatus::FtpStatus(550), // Permission denied
232                                body: b"Upload rejected by rule".to_vec(),
233                                metadata: HashMap::new(),
234                                content_type: "text/plain".to_string(),
235                            })
236                        }
237                    } else {
238                        Ok(ProtocolResponse {
239                            status: ResponseStatus::FtpStatus(550), // Bad request
240                            body: b"No file data provided".to_vec(),
241                            metadata: HashMap::new(),
242                            content_type: "text/plain".to_string(),
243                        })
244                    }
245                } else {
246                    Ok(ProtocolResponse {
247                        status: ResponseStatus::FtpStatus(550), // Permission denied
248                        body: b"No upload rule matches this path".to_vec(),
249                        metadata: HashMap::new(),
250                        content_type: "text/plain".to_string(),
251                    })
252                }
253            }
254            "LIST" => {
255                // Directory listing
256                let path = std::path::Path::new(&request.path);
257                let files = self.vfs.list_files(path);
258                let listing = files
259                    .iter()
260                    .map(|file| {
261                        format!(
262                            "-rw-r--r-- 1 {} {} {} {} {} {}",
263                            file.metadata.owner,
264                            file.metadata.group,
265                            file.metadata.size,
266                            file.modified_at.format("%b %d %H:%M"),
267                            file.path.file_name().unwrap_or_default().to_string_lossy(),
268                            ""
269                        )
270                    })
271                    .collect::<Vec<_>>()
272                    .join("\n");
273
274                Ok(ProtocolResponse {
275                    status: ResponseStatus::FtpStatus(226), // Transfer complete
276                    body: listing.into_bytes(),
277                    metadata: HashMap::from([
278                        ("path".to_string(), request.path.clone()),
279                        ("count".to_string(), files.len().to_string()),
280                    ]),
281                    content_type: "text/plain".to_string(),
282                })
283            }
284            "DELE" => {
285                // Delete file
286                let path = std::path::Path::new(&request.path);
287                if self.vfs.get_file(path).is_some() {
288                    self.vfs
289                        .remove_file(path)
290                        .map_err(|e| mockforge_core::Error::from(e.to_string()))?;
291                    Ok(ProtocolResponse {
292                        status: ResponseStatus::FtpStatus(250), // File deleted
293                        body: b"File deleted".to_vec(),
294                        metadata: HashMap::from([("path".to_string(), request.path.clone())]),
295                        content_type: "text/plain".to_string(),
296                    })
297                } else {
298                    Ok(ProtocolResponse {
299                        status: ResponseStatus::FtpStatus(550), // File not found
300                        body: b"File not found".to_vec(),
301                        metadata: HashMap::new(),
302                        content_type: "text/plain".to_string(),
303                    })
304                }
305            }
306            "PWD" => {
307                // Print working directory
308                Ok(ProtocolResponse {
309                    status: ResponseStatus::FtpStatus(257), // Current directory
310                    body: format!("\"{}\"", request.path).into_bytes(),
311                    metadata: HashMap::from([("path".to_string(), request.path.clone())]),
312                    content_type: "text/plain".to_string(),
313                })
314            }
315            "SIZE" => {
316                // Get file size
317                let path = std::path::Path::new(&request.path);
318                if let Some(file) = self.vfs.get_file(path) {
319                    Ok(ProtocolResponse {
320                        status: ResponseStatus::FtpStatus(213), // File size
321                        body: file.metadata.size.to_string().into_bytes(),
322                        metadata: HashMap::from([
323                            ("path".to_string(), request.path.clone()),
324                            ("size".to_string(), file.metadata.size.to_string()),
325                        ]),
326                        content_type: "text/plain".to_string(),
327                    })
328                } else {
329                    Ok(ProtocolResponse {
330                        status: ResponseStatus::FtpStatus(550), // File not found
331                        body: b"File not found".to_vec(),
332                        metadata: HashMap::new(),
333                        content_type: "text/plain".to_string(),
334                    })
335                }
336            }
337            _ => {
338                Ok(ProtocolResponse {
339                    status: ResponseStatus::FtpStatus(502), // Command not implemented
340                    body: b"Command not implemented".to_vec(),
341                    metadata: HashMap::new(),
342                    content_type: "text/plain".to_string(),
343                })
344            }
345        }
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::fixtures::{
353        FileContentConfig, FileValidation, FtpFixture, UploadRule, UploadStorage, VirtualFileConfig,
354    };
355    use crate::vfs::{FileContent, FileMetadata, VirtualFile};
356
357    #[test]
358    fn test_upload_record_debug() {
359        let record = UploadRecord {
360            id: "test-id".to_string(),
361            path: std::path::PathBuf::from("/test.txt"),
362            size: 1024,
363            uploaded_at: chrono::Utc::now(),
364            rule_name: Some("test-rule".to_string()),
365        };
366        let debug = format!("{:?}", record);
367        assert!(debug.contains("test-id"));
368    }
369
370    #[test]
371    fn test_upload_record_clone() {
372        let record = UploadRecord {
373            id: "test-id".to_string(),
374            path: std::path::PathBuf::from("/test.txt"),
375            size: 1024,
376            uploaded_at: chrono::Utc::now(),
377            rule_name: None,
378        };
379        let cloned = record.clone();
380        assert_eq!(record.id, cloned.id);
381        assert_eq!(record.size, cloned.size);
382    }
383
384    #[test]
385    fn test_ftp_spec_registry_new() {
386        let registry = FtpSpecRegistry::new();
387        assert!(registry.fixtures.is_empty());
388        assert_eq!(registry.get_uploads().len(), 0);
389    }
390
391    #[test]
392    fn test_ftp_spec_registry_default() {
393        let registry = FtpSpecRegistry::default();
394        assert!(registry.fixtures.is_empty());
395    }
396
397    #[test]
398    fn test_ftp_spec_registry_protocol() {
399        let registry = FtpSpecRegistry::new();
400        assert_eq!(registry.protocol(), Protocol::Ftp);
401    }
402
403    #[test]
404    fn test_ftp_spec_registry_with_fixtures() {
405        let fixture = FtpFixture {
406            identifier: "test-fixture".to_string(),
407            name: "Test Fixture".to_string(),
408            description: Some("A test fixture".to_string()),
409            virtual_files: vec![VirtualFileConfig {
410                path: std::path::PathBuf::from("/test.txt"),
411                content: FileContentConfig::Static {
412                    content: "Hello World".to_string(),
413                },
414                permissions: "644".to_string(),
415                owner: "user".to_string(),
416                group: "group".to_string(),
417            }],
418            upload_rules: vec![],
419        };
420
421        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
422
423        assert_eq!(registry.fixtures.len(), 1);
424        assert!(registry.vfs.get_file(&std::path::PathBuf::from("/test.txt")).is_some());
425    }
426
427    #[test]
428    fn test_ftp_spec_registry_with_vfs() {
429        let vfs = Arc::new(VirtualFileSystem::new(std::path::PathBuf::from("/")));
430        let registry = FtpSpecRegistry::new().with_vfs(vfs.clone());
431
432        assert!(Arc::ptr_eq(&registry.vfs, &vfs));
433    }
434
435    #[test]
436    fn test_find_upload_rule_match() {
437        let rule = UploadRule {
438            path_pattern: r"^/uploads/.*\.txt$".to_string(),
439            auto_accept: true,
440            validation: None,
441            storage: UploadStorage::Memory,
442        };
443
444        let fixture = FtpFixture {
445            identifier: "test".to_string(),
446            name: "Test".to_string(),
447            description: None,
448            virtual_files: vec![],
449            upload_rules: vec![rule],
450        };
451
452        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
453
454        assert!(registry.find_upload_rule("/uploads/test.txt").is_some());
455        assert!(registry.find_upload_rule("/uploads/test.pdf").is_none());
456    }
457
458    #[test]
459    fn test_record_upload() {
460        let registry = FtpSpecRegistry::new();
461        let path = std::path::PathBuf::from("/upload.txt");
462
463        let id = registry
464            .record_upload(path.clone(), 1024, Some("test-rule".to_string()))
465            .unwrap();
466
467        assert!(!id.is_empty());
468        let uploads = registry.get_uploads();
469        assert_eq!(uploads.len(), 1);
470        assert_eq!(uploads[0].path, path);
471        assert_eq!(uploads[0].size, 1024);
472    }
473
474    #[test]
475    fn test_get_upload() {
476        let registry = FtpSpecRegistry::new();
477        let id = registry
478            .record_upload(std::path::PathBuf::from("/test.txt"), 512, None)
479            .unwrap();
480
481        let upload = registry.get_upload(&id);
482        assert!(upload.is_some());
483        assert_eq!(upload.unwrap().size, 512);
484    }
485
486    #[test]
487    fn test_get_upload_not_found() {
488        let registry = FtpSpecRegistry::new();
489        let upload = registry.get_upload("non-existent-id");
490        assert!(upload.is_none());
491    }
492
493    #[test]
494    fn test_operations() {
495        let fixture = FtpFixture {
496            identifier: "test".to_string(),
497            name: "Test Fixture".to_string(),
498            description: Some("Test description".to_string()),
499            virtual_files: vec![VirtualFileConfig {
500                path: std::path::PathBuf::from("/file1.txt"),
501                content: FileContentConfig::Static {
502                    content: "content".to_string(),
503                },
504                permissions: "644".to_string(),
505                owner: "user".to_string(),
506                group: "group".to_string(),
507            }],
508            upload_rules: vec![],
509        };
510
511        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
512
513        let ops = registry.operations();
514        assert_eq!(ops.len(), 1);
515        assert_eq!(ops[0].operation_type, "RETR");
516        assert!(ops[0].metadata.contains_key("description"));
517    }
518
519    #[test]
520    fn test_find_operation() {
521        let fixture = FtpFixture {
522            identifier: "test".to_string(),
523            name: "Test".to_string(),
524            description: None,
525            virtual_files: vec![VirtualFileConfig {
526                path: std::path::PathBuf::from("/test.txt"),
527                content: FileContentConfig::Static {
528                    content: "test".to_string(),
529                },
530                permissions: "755".to_string(),
531                owner: "root".to_string(),
532                group: "admin".to_string(),
533            }],
534            upload_rules: vec![],
535        };
536
537        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
538
539        let op = registry.find_operation("RETR", "/test.txt");
540        assert!(op.is_some());
541        let op = op.unwrap();
542        assert_eq!(op.operation_type, "RETR");
543        assert_eq!(op.metadata.get("permissions").unwrap(), "755");
544    }
545
546    #[test]
547    fn test_find_operation_not_found() {
548        let registry = FtpSpecRegistry::new();
549        let op = registry.find_operation("RETR", "/nonexistent.txt");
550        assert!(op.is_none());
551    }
552
553    #[test]
554    fn test_validate_request_invalid_protocol() {
555        let registry = FtpSpecRegistry::new();
556        let request = ProtocolRequest {
557            protocol: Protocol::Http,
558            operation: "RETR".to_string(),
559            path: "/test.txt".to_string(),
560            body: None,
561            ..Default::default()
562        };
563
564        let result = registry.validate_request(&request).unwrap();
565        assert!(!result.valid);
566        assert_eq!(result.errors[0].code, Some("invalid_protocol".to_string()));
567    }
568
569    #[test]
570    fn test_validate_request_invalid_operation() {
571        let registry = FtpSpecRegistry::new();
572        let request = ProtocolRequest {
573            protocol: Protocol::Ftp,
574            operation: "INVALID".to_string(),
575            path: "/test.txt".to_string(),
576            body: None,
577            ..Default::default()
578        };
579
580        let result = registry.validate_request(&request).unwrap();
581        assert!(!result.valid);
582        assert_eq!(result.errors[0].code, Some("unsupported_operation".to_string()));
583    }
584
585    #[test]
586    fn test_validate_request_valid() {
587        let registry = FtpSpecRegistry::new();
588        let request = ProtocolRequest {
589            protocol: Protocol::Ftp,
590            operation: "RETR".to_string(),
591            path: "/test.txt".to_string(),
592            body: None,
593            ..Default::default()
594        };
595
596        let result = registry.validate_request(&request).unwrap();
597        assert!(result.valid);
598    }
599
600    #[test]
601    fn test_generate_mock_response_retr_success() {
602        let registry = FtpSpecRegistry::new();
603        let file = VirtualFile::new(
604            std::path::PathBuf::from("/test.txt"),
605            FileContent::Static(b"test content".to_vec()),
606            FileMetadata {
607                size: 12,
608                ..Default::default()
609            },
610        );
611        registry.vfs.add_file(std::path::PathBuf::from("/test.txt"), file).unwrap();
612
613        let request = ProtocolRequest {
614            protocol: Protocol::Ftp,
615            operation: "RETR".to_string(),
616            path: "/test.txt".to_string(),
617            body: None,
618            ..Default::default()
619        };
620
621        let response = registry.generate_mock_response(&request).unwrap();
622        assert_eq!(response.status, ResponseStatus::FtpStatus(150));
623        assert_eq!(response.body, b"test content");
624    }
625
626    #[test]
627    fn test_generate_mock_response_retr_not_found() {
628        let registry = FtpSpecRegistry::new();
629        let request = ProtocolRequest {
630            protocol: Protocol::Ftp,
631            operation: "RETR".to_string(),
632            path: "/nonexistent.txt".to_string(),
633            body: None,
634            ..Default::default()
635        };
636
637        let response = registry.generate_mock_response(&request).unwrap();
638        assert_eq!(response.status, ResponseStatus::FtpStatus(550));
639    }
640
641    #[test]
642    fn test_generate_mock_response_stor_success() {
643        let rule = UploadRule {
644            path_pattern: r"^/uploads/.*".to_string(),
645            auto_accept: true,
646            validation: None,
647            storage: UploadStorage::Memory,
648        };
649
650        let fixture = FtpFixture {
651            identifier: "test".to_string(),
652            name: "Test".to_string(),
653            description: None,
654            virtual_files: vec![],
655            upload_rules: vec![rule],
656        };
657
658        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
659
660        let request = ProtocolRequest {
661            protocol: Protocol::Ftp,
662            operation: "STOR".to_string(),
663            path: "/uploads/test.txt".to_string(),
664            body: Some(b"file content".to_vec()),
665            ..Default::default()
666        };
667
668        let response = registry.generate_mock_response(&request).unwrap();
669        assert_eq!(response.status, ResponseStatus::FtpStatus(226));
670    }
671
672    #[test]
673    fn test_generate_mock_response_stor_rejected() {
674        let rule = UploadRule {
675            path_pattern: r"^/uploads/.*".to_string(),
676            auto_accept: false,
677            validation: None,
678            storage: UploadStorage::Memory,
679        };
680
681        let fixture = FtpFixture {
682            identifier: "test".to_string(),
683            name: "Test".to_string(),
684            description: None,
685            virtual_files: vec![],
686            upload_rules: vec![rule],
687        };
688
689        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
690
691        let request = ProtocolRequest {
692            protocol: Protocol::Ftp,
693            operation: "STOR".to_string(),
694            path: "/uploads/test.txt".to_string(),
695            body: Some(b"file content".to_vec()),
696            ..Default::default()
697        };
698
699        let response = registry.generate_mock_response(&request).unwrap();
700        assert_eq!(response.status, ResponseStatus::FtpStatus(550));
701        assert_eq!(response.body, b"Upload rejected by rule");
702    }
703
704    #[test]
705    fn test_generate_mock_response_stor_validation_failed() {
706        let rule = UploadRule {
707            path_pattern: r"^/uploads/.*".to_string(),
708            auto_accept: true,
709            validation: Some(FileValidation {
710                max_size_bytes: Some(10),
711                allowed_extensions: None,
712                mime_types: None,
713            }),
714            storage: UploadStorage::Memory,
715        };
716
717        let fixture = FtpFixture {
718            identifier: "test".to_string(),
719            name: "Test".to_string(),
720            description: None,
721            virtual_files: vec![],
722            upload_rules: vec![rule],
723        };
724
725        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
726
727        let request = ProtocolRequest {
728            protocol: Protocol::Ftp,
729            operation: "STOR".to_string(),
730            path: "/uploads/test.txt".to_string(),
731            body: Some(b"very large file content".to_vec()),
732            ..Default::default()
733        };
734
735        let response = registry.generate_mock_response(&request).unwrap();
736        assert_eq!(response.status, ResponseStatus::FtpStatus(550));
737    }
738
739    #[test]
740    fn test_generate_mock_response_stor_no_body() {
741        let rule = UploadRule {
742            path_pattern: r"^/uploads/.*".to_string(),
743            auto_accept: true,
744            validation: None,
745            storage: UploadStorage::Memory,
746        };
747
748        let fixture = FtpFixture {
749            identifier: "test".to_string(),
750            name: "Test".to_string(),
751            description: None,
752            virtual_files: vec![],
753            upload_rules: vec![rule],
754        };
755
756        let registry = FtpSpecRegistry::new().with_fixtures(vec![fixture]).unwrap();
757
758        let request = ProtocolRequest {
759            protocol: Protocol::Ftp,
760            operation: "STOR".to_string(),
761            path: "/uploads/test.txt".to_string(),
762            body: None,
763            ..Default::default()
764        };
765
766        let response = registry.generate_mock_response(&request).unwrap();
767        assert_eq!(response.status, ResponseStatus::FtpStatus(550));
768        assert_eq!(response.body, b"No file data provided");
769    }
770
771    #[test]
772    fn test_generate_mock_response_stor_no_rule() {
773        let registry = FtpSpecRegistry::new();
774        let request = ProtocolRequest {
775            protocol: Protocol::Ftp,
776            operation: "STOR".to_string(),
777            path: "/uploads/test.txt".to_string(),
778            body: Some(b"content".to_vec()),
779            ..Default::default()
780        };
781
782        let response = registry.generate_mock_response(&request).unwrap();
783        assert_eq!(response.status, ResponseStatus::FtpStatus(550));
784        assert_eq!(response.body, b"No upload rule matches this path");
785    }
786
787    #[test]
788    fn test_generate_mock_response_list() {
789        let registry = FtpSpecRegistry::new();
790        let file = VirtualFile::new(
791            std::path::PathBuf::from("/dir/file.txt"),
792            FileContent::Static(b"content".to_vec()),
793            FileMetadata {
794                size: 7,
795                ..Default::default()
796            },
797        );
798        registry.vfs.add_file(std::path::PathBuf::from("/dir/file.txt"), file).unwrap();
799
800        let request = ProtocolRequest {
801            protocol: Protocol::Ftp,
802            operation: "LIST".to_string(),
803            path: "/dir".to_string(),
804            body: None,
805            ..Default::default()
806        };
807
808        let response = registry.generate_mock_response(&request).unwrap();
809        assert_eq!(response.status, ResponseStatus::FtpStatus(226));
810        assert!(response.metadata.get("count").unwrap().parse::<usize>().unwrap() >= 1);
811    }
812
813    #[test]
814    fn test_generate_mock_response_dele_success() {
815        let registry = FtpSpecRegistry::new();
816        let file = VirtualFile::new(
817            std::path::PathBuf::from("/test.txt"),
818            FileContent::Static(vec![]),
819            FileMetadata::default(),
820        );
821        registry.vfs.add_file(std::path::PathBuf::from("/test.txt"), file).unwrap();
822
823        let request = ProtocolRequest {
824            protocol: Protocol::Ftp,
825            operation: "DELE".to_string(),
826            path: "/test.txt".to_string(),
827            body: None,
828            ..Default::default()
829        };
830
831        let response = registry.generate_mock_response(&request).unwrap();
832        assert_eq!(response.status, ResponseStatus::FtpStatus(250));
833    }
834
835    #[test]
836    fn test_generate_mock_response_dele_not_found() {
837        let registry = FtpSpecRegistry::new();
838        let request = ProtocolRequest {
839            protocol: Protocol::Ftp,
840            operation: "DELE".to_string(),
841            path: "/nonexistent.txt".to_string(),
842            body: None,
843            ..Default::default()
844        };
845
846        let response = registry.generate_mock_response(&request).unwrap();
847        assert_eq!(response.status, ResponseStatus::FtpStatus(550));
848    }
849
850    #[test]
851    fn test_generate_mock_response_pwd() {
852        let registry = FtpSpecRegistry::new();
853        let request = ProtocolRequest {
854            protocol: Protocol::Ftp,
855            operation: "PWD".to_string(),
856            path: "/home/user".to_string(),
857            body: None,
858            ..Default::default()
859        };
860
861        let response = registry.generate_mock_response(&request).unwrap();
862        assert_eq!(response.status, ResponseStatus::FtpStatus(257));
863        assert_eq!(response.body, b"\"/home/user\"");
864    }
865
866    #[test]
867    fn test_generate_mock_response_size_success() {
868        let registry = FtpSpecRegistry::new();
869        let file = VirtualFile::new(
870            std::path::PathBuf::from("/test.txt"),
871            FileContent::Static(b"test".to_vec()),
872            FileMetadata {
873                size: 1024,
874                ..Default::default()
875            },
876        );
877        registry.vfs.add_file(std::path::PathBuf::from("/test.txt"), file).unwrap();
878
879        let request = ProtocolRequest {
880            protocol: Protocol::Ftp,
881            operation: "SIZE".to_string(),
882            path: "/test.txt".to_string(),
883            body: None,
884            ..Default::default()
885        };
886
887        let response = registry.generate_mock_response(&request).unwrap();
888        assert_eq!(response.status, ResponseStatus::FtpStatus(213));
889        assert_eq!(response.body, b"1024");
890    }
891
892    #[test]
893    fn test_generate_mock_response_size_not_found() {
894        let registry = FtpSpecRegistry::new();
895        let request = ProtocolRequest {
896            protocol: Protocol::Ftp,
897            operation: "SIZE".to_string(),
898            path: "/nonexistent.txt".to_string(),
899            body: None,
900            ..Default::default()
901        };
902
903        let response = registry.generate_mock_response(&request).unwrap();
904        assert_eq!(response.status, ResponseStatus::FtpStatus(550));
905    }
906
907    #[test]
908    fn test_generate_mock_response_unsupported_command() {
909        let registry = FtpSpecRegistry::new();
910        let request = ProtocolRequest {
911            protocol: Protocol::Ftp,
912            operation: "MKD".to_string(),
913            path: "/newdir".to_string(),
914            body: None,
915            ..Default::default()
916        };
917
918        let response = registry.generate_mock_response(&request).unwrap();
919        assert_eq!(response.status, ResponseStatus::FtpStatus(502));
920        assert_eq!(response.body, b"Command not implemented");
921    }
922}