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