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_ftp_fixture_debug() {
152        let fixture = FtpFixture {
153            identifier: "test-id".to_string(),
154            name: "Test Fixture".to_string(),
155            description: Some("A test fixture".to_string()),
156            virtual_files: vec![],
157            upload_rules: vec![],
158        };
159
160        let debug = format!("{:?}", fixture);
161        assert!(debug.contains("test-id"));
162    }
163
164    #[test]
165    fn test_ftp_fixture_clone() {
166        let fixture = FtpFixture {
167            identifier: "test".to_string(),
168            name: "Test".to_string(),
169            description: None,
170            virtual_files: vec![],
171            upload_rules: vec![],
172        };
173
174        let cloned = fixture.clone();
175        assert_eq!(fixture.identifier, cloned.identifier);
176    }
177
178    #[test]
179    fn test_virtual_file_config_to_file_fixture_static() {
180        let config = VirtualFileConfig {
181            path: std::path::PathBuf::from("/test.txt"),
182            content: FileContentConfig::Static {
183                content: "Hello World".to_string(),
184            },
185            permissions: "644".to_string(),
186            owner: "user".to_string(),
187            group: "group".to_string(),
188        };
189
190        let fixture = config.to_file_fixture();
191        assert_eq!(fixture.path, std::path::PathBuf::from("/test.txt"));
192        assert_eq!(fixture.metadata.permissions, "644");
193    }
194
195    #[test]
196    fn test_virtual_file_config_to_file_fixture_template() {
197        let config = VirtualFileConfig {
198            path: std::path::PathBuf::from("/template.txt"),
199            content: FileContentConfig::Template {
200                template: "Hello {{name}}".to_string(),
201            },
202            permissions: "755".to_string(),
203            owner: "root".to_string(),
204            group: "admin".to_string(),
205        };
206
207        let fixture = config.to_file_fixture();
208        assert_eq!(fixture.metadata.owner, "root");
209    }
210
211    #[test]
212    fn test_virtual_file_config_to_file_fixture_generated() {
213        let config = VirtualFileConfig {
214            path: std::path::PathBuf::from("/generated.bin"),
215            content: FileContentConfig::Generated {
216                size: 1024,
217                pattern: GenerationPattern::Random,
218            },
219            permissions: "600".to_string(),
220            owner: "user".to_string(),
221            group: "user".to_string(),
222        };
223
224        let fixture = config.to_file_fixture();
225        assert_eq!(fixture.metadata.permissions, "600");
226    }
227
228    #[test]
229    fn test_upload_rule_matches_path() {
230        let rule = UploadRule {
231            path_pattern: r"^/uploads/.*\.txt$".to_string(),
232            auto_accept: true,
233            validation: None,
234            storage: UploadStorage::Memory,
235        };
236
237        assert!(rule.matches_path("/uploads/file.txt"));
238        assert!(rule.matches_path("/uploads/test.txt"));
239        assert!(!rule.matches_path("/uploads/file.pdf"));
240        assert!(!rule.matches_path("/other/file.txt"));
241    }
242
243    #[test]
244    fn test_upload_rule_matches_path_invalid_regex() {
245        let rule = UploadRule {
246            path_pattern: "[invalid regex(".to_string(),
247            auto_accept: true,
248            validation: None,
249            storage: UploadStorage::Memory,
250        };
251
252        assert!(!rule.matches_path("/any/path"));
253    }
254
255    #[test]
256    fn test_validate_file_max_size() {
257        let validation = FileValidation {
258            max_size_bytes: Some(100),
259            allowed_extensions: None,
260            mime_types: None,
261        };
262
263        let rule = UploadRule {
264            path_pattern: ".*".to_string(),
265            auto_accept: true,
266            validation: Some(validation),
267            storage: UploadStorage::Discard,
268        };
269
270        // Test file within size limit
271        let small_data = b"small file";
272        assert!(rule.validate_file(small_data, "test.txt").is_ok());
273
274        // Test file exceeding size limit
275        let large_data = vec![0u8; 200];
276        let result = rule.validate_file(&large_data, "test.txt");
277        assert!(result.is_err());
278        assert!(result.unwrap_err().contains("too large"));
279    }
280
281    #[test]
282    fn test_validate_file_extensions() {
283        let validation = FileValidation {
284            max_size_bytes: None,
285            allowed_extensions: Some(vec!["txt".to_string(), "pdf".to_string()]),
286            mime_types: None,
287        };
288
289        let rule = UploadRule {
290            path_pattern: ".*".to_string(),
291            auto_accept: true,
292            validation: Some(validation),
293            storage: UploadStorage::Discard,
294        };
295
296        let data = b"test content";
297
298        // Test valid extensions
299        assert!(rule.validate_file(data, "file.txt").is_ok());
300        assert!(rule.validate_file(data, "document.pdf").is_ok());
301        assert!(rule.validate_file(data, "FILE.TXT").is_ok()); // Case insensitive
302
303        // Test invalid extension
304        let result = rule.validate_file(data, "image.png");
305        assert!(result.is_err());
306        assert!(result.unwrap_err().contains("Invalid file extension"));
307    }
308
309    #[test]
310    fn test_validate_file_mime_type() {
311        let validation = FileValidation {
312            max_size_bytes: None,
313            allowed_extensions: None,
314            mime_types: Some(vec!["text/plain".to_string(), "application/json".to_string()]),
315        };
316
317        let rule = UploadRule {
318            path_pattern: ".*".to_string(),
319            auto_accept: true,
320            validation: Some(validation),
321            storage: UploadStorage::Discard,
322        };
323
324        // Test valid MIME type (text/plain for .txt)
325        let data = b"Hello world";
326        let filename = "test.txt";
327        assert!(rule.validate_file(data, filename).is_ok());
328
329        // Test invalid MIME type (image/png for .txt, but mime_guess guesses text/plain)
330        // Actually, for .txt it guesses text/plain, so let's test with a filename that guesses wrong
331        // mime_guess guesses based on extension, so for "test.png" it would guess image/png
332        let filename_invalid = "test.png";
333        assert!(rule.validate_file(data, filename_invalid).is_err());
334
335        // Test with no MIME types configured
336        let rule_no_mime = UploadRule {
337            path_pattern: ".*".to_string(),
338            auto_accept: true,
339            validation: Some(FileValidation {
340                max_size_bytes: None,
341                allowed_extensions: None,
342                mime_types: None,
343            }),
344            storage: UploadStorage::Discard,
345        };
346        assert!(rule_no_mime.validate_file(data, filename).is_ok());
347    }
348
349    #[test]
350    fn test_validate_file_no_validation() {
351        let rule = UploadRule {
352            path_pattern: ".*".to_string(),
353            auto_accept: true,
354            validation: None,
355            storage: UploadStorage::Discard,
356        };
357
358        let data = vec![0u8; 10000];
359        assert!(rule.validate_file(&data, "any.file").is_ok());
360    }
361
362    #[test]
363    fn test_validate_file_combined_validations() {
364        let validation = FileValidation {
365            max_size_bytes: Some(1000),
366            allowed_extensions: Some(vec!["txt".to_string()]),
367            mime_types: Some(vec!["text/plain".to_string()]),
368        };
369
370        let rule = UploadRule {
371            path_pattern: ".*".to_string(),
372            auto_accept: true,
373            validation: Some(validation),
374            storage: UploadStorage::Discard,
375        };
376
377        let data = b"valid content";
378
379        // All validations pass
380        assert!(rule.validate_file(data, "file.txt").is_ok());
381
382        // Size validation fails
383        let large_data = vec![0u8; 2000];
384        assert!(rule.validate_file(&large_data, "file.txt").is_err());
385
386        // Extension validation fails
387        assert!(rule.validate_file(data, "file.pdf").is_err());
388
389        // MIME type validation fails
390        assert!(rule.validate_file(data, "file.png").is_err());
391    }
392
393    #[test]
394    fn test_file_validation_debug() {
395        let validation = FileValidation {
396            max_size_bytes: Some(100),
397            allowed_extensions: Some(vec!["txt".to_string()]),
398            mime_types: Some(vec!["text/plain".to_string()]),
399        };
400
401        let debug = format!("{:?}", validation);
402        assert!(debug.contains("FileValidation"));
403    }
404
405    #[test]
406    fn test_upload_storage_variants() {
407        let discard = UploadStorage::Discard;
408        let memory = UploadStorage::Memory;
409        let file = UploadStorage::File {
410            path: std::path::PathBuf::from("/tmp/uploads"),
411        };
412
413        let _ = format!("{:?}", discard);
414        let _ = format!("{:?}", memory);
415        let _ = format!("{:?}", file);
416    }
417
418    #[test]
419    fn test_file_content_config_variants() {
420        let static_content = FileContentConfig::Static {
421            content: "test".to_string(),
422        };
423        let template = FileContentConfig::Template {
424            template: "Hello {{name}}".to_string(),
425        };
426        let generated = FileContentConfig::Generated {
427            size: 100,
428            pattern: GenerationPattern::Random,
429        };
430
431        let _ = format!("{:?}", static_content);
432        let _ = format!("{:?}", template);
433        let _ = format!("{:?}", generated);
434    }
435}