Skip to main content

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