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