mockforge_ftp/
vfs.rs

1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use handlebars::Handlebars;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// Virtual File System for FTP server
12#[derive(Debug, Clone)]
13pub struct VirtualFileSystem {
14    #[allow(dead_code)]
15    root: PathBuf,
16    files: Arc<RwLock<HashMap<PathBuf, VirtualFile>>>,
17    fixtures: Arc<RwLock<HashMap<PathBuf, FileFixture>>>,
18}
19
20impl VirtualFileSystem {
21    pub fn new(root: PathBuf) -> Self {
22        Self {
23            root,
24            files: Arc::new(RwLock::new(HashMap::new())),
25            fixtures: Arc::new(RwLock::new(HashMap::new())),
26        }
27    }
28
29    pub fn add_file(&self, path: PathBuf, file: VirtualFile) -> Result<()> {
30        let mut files = self.files.blocking_write();
31        files.insert(path, file);
32        Ok(())
33    }
34
35    pub fn get_file(&self, path: &Path) -> Option<VirtualFile> {
36        let files = self.files.blocking_read();
37        if let Some(file) = files.get(path) {
38            return Some(file.clone());
39        }
40
41        // Check fixtures
42        let fixtures = self.fixtures.blocking_read();
43        if let Some(fixture) = fixtures.get(path) {
44            return Some(fixture.clone().to_virtual_file());
45        }
46
47        None
48    }
49
50    pub fn remove_file(&self, path: &Path) -> Result<()> {
51        let mut files = self.files.blocking_write();
52        files.remove(path);
53        Ok(())
54    }
55
56    pub fn list_files(&self, path: &Path) -> Vec<VirtualFile> {
57        let files = self.files.blocking_read();
58        files
59            .iter()
60            .filter(|(file_path, _)| file_path.starts_with(path))
61            .map(|(_, file)| file.clone())
62            .collect()
63    }
64
65    pub fn clear(&self) -> Result<()> {
66        let mut files = self.files.blocking_write();
67        files.clear();
68        Ok(())
69    }
70
71    pub fn add_fixture(&self, fixture: FileFixture) -> Result<()> {
72        let mut fixtures = self.fixtures.blocking_write();
73        fixtures.insert(fixture.path.clone(), fixture);
74        Ok(())
75    }
76
77    pub fn load_fixtures(&self, fixtures: Vec<FileFixture>) -> Result<()> {
78        for fixture in fixtures {
79            self.add_fixture(fixture)?;
80        }
81        Ok(())
82    }
83
84    /// Async version of add_file - use this in async contexts
85    pub async fn add_file_async(&self, path: PathBuf, file: VirtualFile) -> Result<()> {
86        let mut files = self.files.write().await;
87        files.insert(path, file);
88        Ok(())
89    }
90
91    /// Async version of get_file - use this in async contexts
92    pub async fn get_file_async(&self, path: &Path) -> Option<VirtualFile> {
93        let files = self.files.read().await;
94        if let Some(file) = files.get(path) {
95            return Some(file.clone());
96        }
97
98        // Check fixtures
99        let fixtures = self.fixtures.read().await;
100        if let Some(fixture) = fixtures.get(path) {
101            return Some(fixture.clone().to_virtual_file());
102        }
103
104        None
105    }
106}
107
108/// Virtual file representation
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct VirtualFile {
111    pub path: PathBuf,
112    pub content: FileContent,
113    pub metadata: FileMetadata,
114    pub created_at: DateTime<Utc>,
115    pub modified_at: DateTime<Utc>,
116}
117
118impl VirtualFile {
119    pub fn new(path: PathBuf, content: FileContent, metadata: FileMetadata) -> Self {
120        let now = Utc::now();
121        Self {
122            path,
123            content,
124            metadata,
125            created_at: now,
126            modified_at: now,
127        }
128    }
129
130    pub fn render_content(&self) -> Result<Vec<u8>> {
131        match &self.content {
132            FileContent::Static(data) => Ok(data.clone()),
133            FileContent::Template(template) => {
134                // Render template using Handlebars
135                let handlebars = Handlebars::new();
136                let context = create_template_context();
137                let rendered = handlebars.render_template(template, &context)?;
138                Ok(rendered.into_bytes())
139            }
140            FileContent::Generated { size, pattern } => match pattern {
141                GenerationPattern::Random => Ok((0..*size).map(|_| rand::random::<u8>()).collect()),
142                GenerationPattern::Zeros => Ok(vec![0; *size]),
143                GenerationPattern::Ones => Ok(vec![1; *size]),
144                GenerationPattern::Incremental => Ok((0..*size).map(|i| (i % 256) as u8).collect()),
145            },
146        }
147    }
148}
149
150/// File content types
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum FileContent {
153    Static(Vec<u8>),
154    Template(String),
155    Generated {
156        size: usize,
157        pattern: GenerationPattern,
158    },
159}
160
161/// File metadata
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct FileMetadata {
164    pub permissions: String,
165    pub owner: String,
166    pub group: String,
167    pub size: u64,
168}
169
170impl Default for FileMetadata {
171    fn default() -> Self {
172        Self {
173            permissions: "644".to_string(),
174            owner: "mockforge".to_string(),
175            group: "users".to_string(),
176            size: 0,
177        }
178    }
179}
180
181/// Generation patterns for synthetic files
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub enum GenerationPattern {
184    Random,
185    Zeros,
186    Ones,
187    Incremental,
188}
189
190/// File fixture for configuration
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct FileFixture {
193    pub path: PathBuf,
194    pub content: FileContent,
195    pub metadata: FileMetadata,
196}
197
198impl FileFixture {
199    pub fn to_virtual_file(self) -> VirtualFile {
200        VirtualFile::new(self.path, self.content, self.metadata)
201    }
202}
203
204/// Create a template context with common variables for template rendering
205fn create_template_context() -> Value {
206    let mut context = serde_json::Map::new();
207
208    // Add current timestamp
209    let now = Utc::now();
210    context.insert("now".to_string(), Value::String(now.to_rfc3339()));
211    context.insert("timestamp".to_string(), Value::Number(now.timestamp().into()));
212    context.insert("date".to_string(), Value::String(now.format("%Y-%m-%d").to_string()));
213    context.insert("time".to_string(), Value::String(now.format("%H:%M:%S").to_string()));
214
215    // Add random values
216    context.insert("random_int".to_string(), Value::Number(rand::random::<i64>().into()));
217    context.insert(
218        "random_float".to_string(),
219        Value::String(format!("{:.6}", rand::random::<f64>())),
220    );
221
222    // Add UUID
223    context.insert("uuid".to_string(), Value::String(uuid::Uuid::new_v4().to_string()));
224
225    // Add some sample data
226    let mut faker = serde_json::Map::new();
227    faker.insert("name".to_string(), Value::String("John Doe".to_string()));
228    faker.insert("email".to_string(), Value::String("john.doe@example.com".to_string()));
229    faker.insert("age".to_string(), Value::Number(30.into()));
230    context.insert("faker".to_string(), Value::Object(faker));
231
232    Value::Object(context)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_file_metadata_default() {
241        let metadata = FileMetadata::default();
242        assert_eq!(metadata.permissions, "644");
243        assert_eq!(metadata.owner, "mockforge");
244        assert_eq!(metadata.group, "users");
245        assert_eq!(metadata.size, 0);
246    }
247
248    #[test]
249    fn test_file_metadata_clone() {
250        let metadata = FileMetadata {
251            permissions: "755".to_string(),
252            owner: "root".to_string(),
253            group: "root".to_string(),
254            size: 1024,
255        };
256
257        let cloned = metadata.clone();
258        assert_eq!(metadata.permissions, cloned.permissions);
259        assert_eq!(metadata.owner, cloned.owner);
260        assert_eq!(metadata.size, cloned.size);
261    }
262
263    #[test]
264    fn test_generation_pattern_clone() {
265        let pattern = GenerationPattern::Random;
266        let _cloned = pattern.clone();
267        // Just verify it can be cloned
268    }
269
270    #[test]
271    fn test_generation_pattern_debug() {
272        let pattern = GenerationPattern::Zeros;
273        let debug = format!("{:?}", pattern);
274        assert!(debug.contains("Zeros"));
275    }
276
277    #[test]
278    fn test_file_content_static() {
279        let content = FileContent::Static(b"hello world".to_vec());
280        let debug = format!("{:?}", content);
281        assert!(debug.contains("Static"));
282    }
283
284    #[test]
285    fn test_file_content_template() {
286        let content = FileContent::Template("Hello {{name}}".to_string());
287        let debug = format!("{:?}", content);
288        assert!(debug.contains("Template"));
289    }
290
291    #[test]
292    fn test_file_content_generated() {
293        let content = FileContent::Generated {
294            size: 100,
295            pattern: GenerationPattern::Ones,
296        };
297        let debug = format!("{:?}", content);
298        assert!(debug.contains("Generated"));
299    }
300
301    #[test]
302    fn test_virtual_file_new() {
303        let file = VirtualFile::new(
304            PathBuf::from("/test.txt"),
305            FileContent::Static(b"content".to_vec()),
306            FileMetadata::default(),
307        );
308
309        assert_eq!(file.path, PathBuf::from("/test.txt"));
310    }
311
312    #[test]
313    fn test_virtual_file_render_static() {
314        let file = VirtualFile::new(
315            PathBuf::from("/test.txt"),
316            FileContent::Static(b"hello".to_vec()),
317            FileMetadata::default(),
318        );
319
320        let content = file.render_content().unwrap();
321        assert_eq!(content, b"hello".to_vec());
322    }
323
324    #[test]
325    fn test_virtual_file_render_generated_zeros() {
326        let file = VirtualFile::new(
327            PathBuf::from("/zeros.bin"),
328            FileContent::Generated {
329                size: 10,
330                pattern: GenerationPattern::Zeros,
331            },
332            FileMetadata::default(),
333        );
334
335        let content = file.render_content().unwrap();
336        assert_eq!(content.len(), 10);
337        assert!(content.iter().all(|&b| b == 0));
338    }
339
340    #[test]
341    fn test_virtual_file_render_generated_ones() {
342        let file = VirtualFile::new(
343            PathBuf::from("/ones.bin"),
344            FileContent::Generated {
345                size: 10,
346                pattern: GenerationPattern::Ones,
347            },
348            FileMetadata::default(),
349        );
350
351        let content = file.render_content().unwrap();
352        assert_eq!(content.len(), 10);
353        assert!(content.iter().all(|&b| b == 1));
354    }
355
356    #[test]
357    fn test_virtual_file_render_generated_incremental() {
358        let file = VirtualFile::new(
359            PathBuf::from("/inc.bin"),
360            FileContent::Generated {
361                size: 256,
362                pattern: GenerationPattern::Incremental,
363            },
364            FileMetadata::default(),
365        );
366
367        let content = file.render_content().unwrap();
368        assert_eq!(content.len(), 256);
369        for (i, &b) in content.iter().enumerate() {
370            assert_eq!(b, i as u8);
371        }
372    }
373
374    #[test]
375    fn test_virtual_file_render_generated_random() {
376        let file = VirtualFile::new(
377            PathBuf::from("/random.bin"),
378            FileContent::Generated {
379                size: 100,
380                pattern: GenerationPattern::Random,
381            },
382            FileMetadata::default(),
383        );
384
385        let content = file.render_content().unwrap();
386        assert_eq!(content.len(), 100);
387    }
388
389    #[test]
390    fn test_virtual_file_clone() {
391        let file = VirtualFile::new(
392            PathBuf::from("/test.txt"),
393            FileContent::Static(b"test".to_vec()),
394            FileMetadata::default(),
395        );
396
397        let cloned = file.clone();
398        assert_eq!(file.path, cloned.path);
399    }
400
401    #[test]
402    fn test_virtual_file_debug() {
403        let file = VirtualFile::new(
404            PathBuf::from("/test.txt"),
405            FileContent::Static(vec![]),
406            FileMetadata::default(),
407        );
408
409        let debug = format!("{:?}", file);
410        assert!(debug.contains("VirtualFile"));
411    }
412
413    #[test]
414    fn test_file_fixture_to_virtual_file() {
415        let fixture = FileFixture {
416            path: PathBuf::from("/fixture.txt"),
417            content: FileContent::Static(b"fixture content".to_vec()),
418            metadata: FileMetadata::default(),
419        };
420
421        let file = fixture.to_virtual_file();
422        assert_eq!(file.path, PathBuf::from("/fixture.txt"));
423    }
424
425    #[test]
426    fn test_file_fixture_clone() {
427        let fixture = FileFixture {
428            path: PathBuf::from("/test.txt"),
429            content: FileContent::Static(vec![]),
430            metadata: FileMetadata::default(),
431        };
432
433        let cloned = fixture.clone();
434        assert_eq!(fixture.path, cloned.path);
435    }
436
437    #[test]
438    fn test_vfs_new() {
439        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
440        let files = vfs.list_files(&PathBuf::from("/"));
441        assert!(files.is_empty());
442    }
443
444    #[test]
445    fn test_vfs_add_file() {
446        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
447        let file = VirtualFile::new(
448            PathBuf::from("/test.txt"),
449            FileContent::Static(b"hello".to_vec()),
450            FileMetadata::default(),
451        );
452
453        vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
454
455        let retrieved = vfs.get_file(&PathBuf::from("/test.txt"));
456        assert!(retrieved.is_some());
457    }
458
459    #[test]
460    fn test_vfs_get_file_not_found() {
461        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
462        let retrieved = vfs.get_file(&PathBuf::from("/nonexistent.txt"));
463        assert!(retrieved.is_none());
464    }
465
466    #[test]
467    fn test_vfs_remove_file() {
468        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
469        let file = VirtualFile::new(
470            PathBuf::from("/test.txt"),
471            FileContent::Static(vec![]),
472            FileMetadata::default(),
473        );
474
475        vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
476        vfs.remove_file(&PathBuf::from("/test.txt")).unwrap();
477
478        let retrieved = vfs.get_file(&PathBuf::from("/test.txt"));
479        assert!(retrieved.is_none());
480    }
481
482    #[test]
483    fn test_vfs_list_files() {
484        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
485
486        let file1 = VirtualFile::new(
487            PathBuf::from("/dir/file1.txt"),
488            FileContent::Static(vec![]),
489            FileMetadata::default(),
490        );
491        let file2 = VirtualFile::new(
492            PathBuf::from("/dir/file2.txt"),
493            FileContent::Static(vec![]),
494            FileMetadata::default(),
495        );
496
497        vfs.add_file(PathBuf::from("/dir/file1.txt"), file1).unwrap();
498        vfs.add_file(PathBuf::from("/dir/file2.txt"), file2).unwrap();
499
500        let files = vfs.list_files(&PathBuf::from("/dir"));
501        assert_eq!(files.len(), 2);
502    }
503
504    #[test]
505    fn test_vfs_clear() {
506        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
507
508        let file = VirtualFile::new(
509            PathBuf::from("/test.txt"),
510            FileContent::Static(vec![]),
511            FileMetadata::default(),
512        );
513        vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
514
515        vfs.clear().unwrap();
516
517        let files = vfs.list_files(&PathBuf::from("/"));
518        assert!(files.is_empty());
519    }
520
521    #[test]
522    fn test_vfs_add_fixture() {
523        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
524        let fixture = FileFixture {
525            path: PathBuf::from("/fixture.txt"),
526            content: FileContent::Static(b"fixture".to_vec()),
527            metadata: FileMetadata::default(),
528        };
529
530        vfs.add_fixture(fixture).unwrap();
531
532        let file = vfs.get_file(&PathBuf::from("/fixture.txt"));
533        assert!(file.is_some());
534    }
535
536    #[test]
537    fn test_vfs_load_fixtures() {
538        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
539        let fixtures = vec![
540            FileFixture {
541                path: PathBuf::from("/f1.txt"),
542                content: FileContent::Static(vec![]),
543                metadata: FileMetadata::default(),
544            },
545            FileFixture {
546                path: PathBuf::from("/f2.txt"),
547                content: FileContent::Static(vec![]),
548                metadata: FileMetadata::default(),
549            },
550        ];
551
552        vfs.load_fixtures(fixtures).unwrap();
553
554        assert!(vfs.get_file(&PathBuf::from("/f1.txt")).is_some());
555        assert!(vfs.get_file(&PathBuf::from("/f2.txt")).is_some());
556    }
557
558    #[test]
559    fn test_vfs_clone() {
560        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
561        let _cloned = vfs.clone();
562        // Just verify it can be cloned
563    }
564
565    #[test]
566    fn test_vfs_debug() {
567        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
568        let debug = format!("{:?}", vfs);
569        assert!(debug.contains("VirtualFileSystem"));
570    }
571
572    #[test]
573    fn test_template_context_has_expected_fields() {
574        let context = create_template_context();
575        assert!(context.get("now").is_some());
576        assert!(context.get("timestamp").is_some());
577        assert!(context.get("date").is_some());
578        assert!(context.get("uuid").is_some());
579        assert!(context.get("faker").is_some());
580    }
581
582    #[test]
583    fn test_virtual_file_render_template() {
584        let file = VirtualFile::new(
585            PathBuf::from("/template.txt"),
586            FileContent::Template("Hello {{faker.name}}!".to_string()),
587            FileMetadata::default(),
588        );
589
590        let content = file.render_content().unwrap();
591        let text = String::from_utf8(content).unwrap();
592        assert!(text.contains("Hello"));
593        assert!(text.contains("John Doe")); // From the faker context
594    }
595
596    #[test]
597    fn test_virtual_file_render_template_with_timestamp() {
598        let file = VirtualFile::new(
599            PathBuf::from("/timestamp.txt"),
600            FileContent::Template("Current timestamp: {{timestamp}}".to_string()),
601            FileMetadata::default(),
602        );
603
604        let content = file.render_content().unwrap();
605        let text = String::from_utf8(content).unwrap();
606        assert!(text.contains("Current timestamp:"));
607    }
608
609    #[test]
610    fn test_virtual_file_render_template_with_uuid() {
611        let file = VirtualFile::new(
612            PathBuf::from("/uuid.txt"),
613            FileContent::Template("ID: {{uuid}}".to_string()),
614            FileMetadata::default(),
615        );
616
617        let content = file.render_content().unwrap();
618        let text = String::from_utf8(content).unwrap();
619        assert!(text.starts_with("ID: "));
620        // UUID should be present and not empty
621        let uuid_part = text.trim_start_matches("ID: ");
622        assert!(!uuid_part.is_empty());
623    }
624
625    #[test]
626    fn test_vfs_get_file_from_fixtures() {
627        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
628        let fixture = FileFixture {
629            path: PathBuf::from("/fixture.txt"),
630            content: FileContent::Static(b"fixture content".to_vec()),
631            metadata: FileMetadata::default(),
632        };
633
634        vfs.add_fixture(fixture).unwrap();
635
636        let file = vfs.get_file(&PathBuf::from("/fixture.txt"));
637        assert!(file.is_some());
638        let content = file.unwrap().render_content().unwrap();
639        assert_eq!(content, b"fixture content");
640    }
641
642    #[test]
643    fn test_vfs_files_priority_over_fixtures() {
644        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
645
646        // Add a fixture
647        let fixture = FileFixture {
648            path: PathBuf::from("/test.txt"),
649            content: FileContent::Static(b"fixture".to_vec()),
650            metadata: FileMetadata::default(),
651        };
652        vfs.add_fixture(fixture).unwrap();
653
654        // Add a regular file with same path
655        let file = VirtualFile::new(
656            PathBuf::from("/test.txt"),
657            FileContent::Static(b"file".to_vec()),
658            FileMetadata::default(),
659        );
660        vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
661
662        // Files should take priority over fixtures
663        let retrieved = vfs.get_file(&PathBuf::from("/test.txt")).unwrap();
664        let content = retrieved.render_content().unwrap();
665        assert_eq!(content, b"file");
666    }
667
668    #[test]
669    fn test_vfs_list_files_empty_path() {
670        let vfs = VirtualFileSystem::new(PathBuf::from("/"));
671
672        let file1 = VirtualFile::new(
673            PathBuf::from("/file1.txt"),
674            FileContent::Static(vec![]),
675            FileMetadata::default(),
676        );
677        let file2 = VirtualFile::new(
678            PathBuf::from("/subdir/file2.txt"),
679            FileContent::Static(vec![]),
680            FileMetadata::default(),
681        );
682
683        vfs.add_file(PathBuf::from("/file1.txt"), file1).unwrap();
684        vfs.add_file(PathBuf::from("/subdir/file2.txt"), file2).unwrap();
685
686        // List all files from root
687        let files = vfs.list_files(&PathBuf::from("/"));
688        assert_eq!(files.len(), 2);
689    }
690
691    #[test]
692    fn test_virtual_file_serialization() {
693        let file = VirtualFile::new(
694            PathBuf::from("/test.txt"),
695            FileContent::Static(b"test".to_vec()),
696            FileMetadata::default(),
697        );
698
699        // Test serialization
700        let serialized = serde_json::to_string(&file);
701        assert!(serialized.is_ok());
702
703        // Test deserialization
704        let deserialized: Result<VirtualFile, _> = serde_json::from_str(&serialized.unwrap());
705        assert!(deserialized.is_ok());
706    }
707
708    #[test]
709    fn test_file_metadata_serialization() {
710        let metadata = FileMetadata {
711            permissions: "755".to_string(),
712            owner: "root".to_string(),
713            group: "admin".to_string(),
714            size: 2048,
715        };
716
717        let serialized = serde_json::to_string(&metadata);
718        assert!(serialized.is_ok());
719
720        let deserialized: Result<FileMetadata, _> = serde_json::from_str(&serialized.unwrap());
721        assert!(deserialized.is_ok());
722    }
723
724    #[test]
725    fn test_file_content_serialization() {
726        let content = FileContent::Static(b"test content".to_vec());
727        let serialized = serde_json::to_string(&content);
728        assert!(serialized.is_ok());
729    }
730
731    #[test]
732    fn test_generation_pattern_serialization() {
733        let pattern = GenerationPattern::Random;
734        let serialized = serde_json::to_string(&pattern);
735        assert!(serialized.is_ok());
736    }
737}