mockforge_ftp/
fixtures.rs

1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5use crate::GenerationPattern;
6
7/// FTP fixture configuration
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FtpFixture {
10    pub identifier: String,
11    pub name: String,
12    pub description: Option<String>,
13    pub virtual_files: Vec<VirtualFileConfig>,
14    pub upload_rules: Vec<UploadRule>,
15}
16
17/// Configuration for virtual files
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct VirtualFileConfig {
20    pub path: PathBuf,
21    pub content: FileContentConfig,
22    pub permissions: String,
23    pub owner: String,
24    pub group: String,
25}
26
27/// File content configuration
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(tag = "type")]
30pub enum FileContentConfig {
31    #[serde(rename = "static")]
32    Static { content: String },
33    #[serde(rename = "template")]
34    Template { template: String },
35    #[serde(rename = "generated")]
36    Generated {
37        size: usize,
38        pattern: GenerationPattern,
39    },
40}
41
42/// Upload rule configuration
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct UploadRule {
45    pub path_pattern: String,
46    pub auto_accept: bool,
47    pub validation: Option<FileValidation>,
48    pub storage: UploadStorage,
49}
50
51/// File validation rules
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FileValidation {
54    pub max_size_bytes: Option<u64>,
55    pub allowed_extensions: Option<Vec<String>>,
56    pub mime_types: Option<Vec<String>>,
57}
58
59/// Upload storage options
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(tag = "type")]
62pub enum UploadStorage {
63    #[serde(rename = "discard")]
64    Discard,
65    #[serde(rename = "memory")]
66    Memory,
67    #[serde(rename = "file")]
68    File { path: PathBuf },
69}
70
71impl VirtualFileConfig {
72    pub fn to_file_fixture(self) -> crate::vfs::FileFixture {
73        let content = match self.content {
74            FileContentConfig::Static { content } => {
75                crate::vfs::FileContent::Static(content.into_bytes())
76            }
77            FileContentConfig::Template { template } => crate::vfs::FileContent::Template(template),
78            FileContentConfig::Generated { size, pattern } => {
79                crate::vfs::FileContent::Generated { size, pattern }
80            }
81        };
82
83        let metadata = crate::vfs::FileMetadata {
84            permissions: self.permissions,
85            owner: self.owner,
86            group: self.group,
87            size: 0, // Will be calculated when rendered
88        };
89
90        crate::vfs::FileFixture {
91            path: self.path,
92            content,
93            metadata,
94        }
95    }
96}
97
98impl UploadRule {
99    pub fn matches_path(&self, path: &str) -> bool {
100        match Regex::new(&self.path_pattern) {
101            Ok(regex) => regex.is_match(path),
102            Err(_) => false,
103        }
104    }
105
106    pub fn validate_file(&self, data: &[u8], filename: &str) -> Result<(), String> {
107        if let Some(validation) = &self.validation {
108            // Check file size
109            if let Some(max_size) = validation.max_size_bytes {
110                if data.len() as u64 > max_size {
111                    return Err(format!(
112                        "File too large: {} bytes (max: {})",
113                        data.len(),
114                        max_size
115                    ));
116                }
117            }
118
119            // Check file extension
120            if let Some(extensions) = &validation.allowed_extensions {
121                let has_valid_ext = extensions.iter().any(|ext| {
122                    filename.to_lowercase().ends_with(&format!(".{}", ext.to_lowercase()))
123                });
124                if !extensions.is_empty() && !has_valid_ext {
125                    return Err(format!("Invalid file extension. Allowed: {:?}", extensions));
126                }
127            }
128
129            // Check MIME type
130            if let Some(mime_types) = &validation.mime_types {
131                let guessed = mime_guess::from_path(filename).first_or_octet_stream();
132                let guessed_str = guessed.as_ref();
133                if !mime_types.iter().any(|allowed| allowed == guessed_str) {
134                    return Err(format!(
135                        "Invalid MIME type: {} (allowed: {:?})",
136                        guessed_str, mime_types
137                    ));
138                }
139            }
140        }
141
142        Ok(())
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_validate_file_mime_type() {
152        let validation = FileValidation {
153            max_size_bytes: None,
154            allowed_extensions: None,
155            mime_types: Some(vec!["text/plain".to_string(), "application/json".to_string()]),
156        };
157
158        let rule = UploadRule {
159            path_pattern: ".*".to_string(),
160            auto_accept: true,
161            validation: Some(validation),
162            storage: UploadStorage::Discard,
163        };
164
165        // Test valid MIME type (text/plain for .txt)
166        let data = b"Hello world";
167        let filename = "test.txt";
168        assert!(rule.validate_file(data, filename).is_ok());
169
170        // Test invalid MIME type (image/png for .txt, but mime_guess guesses text/plain)
171        // Actually, for .txt it guesses text/plain, so let's test with a filename that guesses wrong
172        // mime_guess guesses based on extension, so for "test.png" it would guess image/png
173        let filename_invalid = "test.png";
174        assert!(rule.validate_file(data, filename_invalid).is_err());
175
176        // Test with no MIME types configured
177        let rule_no_mime = UploadRule {
178            path_pattern: ".*".to_string(),
179            auto_accept: true,
180            validation: Some(FileValidation {
181                max_size_bytes: None,
182                allowed_extensions: None,
183                mime_types: None,
184            }),
185            storage: UploadStorage::Discard,
186        };
187        assert!(rule_no_mime.validate_file(data, filename).is_ok());
188    }
189}