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#[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#[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 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 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 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), 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), body: b"File not found".to_vec(),
199 metadata: HashMap::new(),
200 content_type: "text/plain".to_string(),
201 })
202 }
203 }
204 "STOR" => {
205 let path = &request.path;
207 if let Some(rule) = self.find_upload_rule(path) {
208 if let Some(body) = &request.body {
209 if let Err(validation_error) = rule.validate_file(body, path) {
211 return Ok(ProtocolResponse {
212 status: ResponseStatus::FtpStatus(550), 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), 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), 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), 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), 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 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), 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 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), 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), body: b"File not found".to_vec(),
301 metadata: HashMap::new(),
302 content_type: "text/plain".to_string(),
303 })
304 }
305 }
306 "PWD" => {
307 Ok(ProtocolResponse {
309 status: ResponseStatus::FtpStatus(257), 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 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), 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), 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), 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(®istry.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}