mockforge_ftp/
server.rs

1use crate::spec_registry::FtpSpecRegistry;
2use crate::storage::MockForgeStorage;
3use crate::vfs::VirtualFileSystem;
4use anyhow::Result;
5use libunftp::ServerBuilder;
6use mockforge_core::config::FtpConfig;
7use std::sync::Arc;
8
9/// FTP Server implementation for MockForge
10#[derive(Debug)]
11pub struct FtpServer {
12    config: FtpConfig,
13    vfs: Arc<VirtualFileSystem>,
14    spec_registry: Arc<FtpSpecRegistry>,
15}
16
17impl FtpServer {
18    pub fn new(config: FtpConfig) -> Self {
19        let vfs = Arc::new(VirtualFileSystem::new(config.virtual_root.clone()));
20        let spec_registry = Arc::new(FtpSpecRegistry::new().with_vfs(vfs.clone()));
21
22        Self {
23            config,
24            vfs,
25            spec_registry,
26        }
27    }
28
29    pub async fn start(&self) -> Result<()> {
30        let addr = format!("{}:{}", self.config.host, self.config.port);
31        println!("Starting FTP server on {}", addr);
32
33        // Create the storage backend
34        let storage = MockForgeStorage::new(self.vfs.clone(), self.spec_registry.clone());
35
36        // Create the FTP server with our custom storage
37        let server = ServerBuilder::new(Box::new(move || storage.clone()))
38            .greeting("MockForge FTP Server")
39            .passive_ports(49152..=65534); // Use dynamic port range for passive mode
40
41        println!("FTP server listening on {}", addr);
42        let server = server.build()?;
43        server.listen(&addr).await?;
44
45        Ok(())
46    }
47
48    pub async fn handle_upload(&self, path: &std::path::Path, data: Vec<u8>) -> Result<()> {
49        // Handle file upload through fixtures
50        let path_str = path.to_string_lossy();
51
52        // Find matching upload rule
53        if let Some(rule) = self.spec_registry.find_upload_rule(&path_str) {
54            // Validate the upload
55            rule.validate_file(&data, &path_str).map_err(|e| anyhow::anyhow!(e))?;
56
57            if rule.auto_accept {
58                // Store the file based on rule
59                match &rule.storage {
60                    crate::fixtures::UploadStorage::Memory => {
61                        // Store in VFS
62                        let size = data.len() as u64;
63                        let file = crate::vfs::VirtualFile::new(
64                            path.to_path_buf(),
65                            crate::vfs::FileContent::Static(data),
66                            crate::vfs::FileMetadata {
67                                size,
68                                ..Default::default()
69                            },
70                        );
71                        self.vfs.add_file_async(path.to_path_buf(), file).await?;
72                    }
73                    crate::fixtures::UploadStorage::File { path: storage_path } => {
74                        // Write to file system
75                        tokio::fs::write(storage_path, &data).await?;
76                    }
77                    crate::fixtures::UploadStorage::Discard => {
78                        // Do nothing
79                    }
80                }
81            }
82        }
83
84        Ok(())
85    }
86
87    pub fn spec_registry(&self) -> Arc<FtpSpecRegistry> {
88        self.spec_registry.clone()
89    }
90
91    pub fn vfs(&self) -> Arc<VirtualFileSystem> {
92        self.vfs.clone()
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::fixtures::{FileValidation, UploadRule, UploadStorage};
100
101    #[test]
102    fn test_ftp_server_new() {
103        let config = FtpConfig {
104            host: "127.0.0.1".to_string(),
105            port: 2121,
106            virtual_root: std::path::PathBuf::from("/"),
107            ..Default::default()
108        };
109
110        let server = FtpServer::new(config.clone());
111        assert_eq!(server.config.host, "127.0.0.1");
112        assert_eq!(server.config.port, 2121);
113    }
114
115    #[test]
116    fn test_ftp_server_debug() {
117        let config = FtpConfig {
118            host: "localhost".to_string(),
119            port: 21,
120            virtual_root: std::path::PathBuf::from("/tmp"),
121            ..Default::default()
122        };
123
124        let server = FtpServer::new(config);
125        let debug = format!("{:?}", server);
126        assert!(debug.contains("FtpServer"));
127    }
128
129    #[test]
130    fn test_ftp_server_spec_registry() {
131        let config = FtpConfig {
132            host: "127.0.0.1".to_string(),
133            port: 2121,
134            virtual_root: std::path::PathBuf::from("/"),
135            ..Default::default()
136        };
137
138        let server = FtpServer::new(config);
139        let registry = server.spec_registry();
140        assert!(registry.fixtures.is_empty());
141    }
142
143    #[test]
144    fn test_ftp_server_vfs() {
145        let config = FtpConfig {
146            host: "127.0.0.1".to_string(),
147            port: 2121,
148            virtual_root: std::path::PathBuf::from("/test"),
149            ..Default::default()
150        };
151
152        let server = FtpServer::new(config);
153        let vfs = server.vfs();
154        let files = vfs.list_files(&std::path::PathBuf::from("/"));
155        assert!(files.is_empty());
156    }
157
158    #[tokio::test]
159    async fn test_handle_upload_memory_storage() {
160        let config = FtpConfig {
161            host: "127.0.0.1".to_string(),
162            port: 2121,
163            virtual_root: std::path::PathBuf::from("/"),
164            ..Default::default()
165        };
166
167        let server = FtpServer::new(config);
168
169        // Create a fixture with an upload rule
170        let rule = UploadRule {
171            path_pattern: r"^/uploads/.*".to_string(),
172            auto_accept: true,
173            validation: None,
174            storage: UploadStorage::Memory,
175        };
176
177        let fixture = crate::fixtures::FtpFixture {
178            identifier: "test".to_string(),
179            name: "Test".to_string(),
180            description: None,
181            virtual_files: vec![],
182            upload_rules: vec![rule],
183        };
184
185        // Update the spec registry
186        let new_registry = FtpSpecRegistry::new()
187            .with_vfs(server.vfs.clone())
188            .with_fixtures(vec![fixture])
189            .unwrap();
190
191        let server = FtpServer {
192            config: server.config,
193            vfs: server.vfs.clone(),
194            spec_registry: Arc::new(new_registry),
195        };
196
197        let path = std::path::Path::new("/uploads/test.txt");
198        let data = b"test file content".to_vec();
199
200        let result = server.handle_upload(path, data.clone()).await;
201        assert!(result.is_ok());
202
203        // Verify file was stored in VFS
204        let file = server.vfs.get_file_async(path).await;
205        assert!(file.is_some());
206    }
207
208    #[tokio::test]
209    async fn test_handle_upload_discard_storage() {
210        let config = FtpConfig {
211            host: "127.0.0.1".to_string(),
212            port: 2121,
213            virtual_root: std::path::PathBuf::from("/"),
214            ..Default::default()
215        };
216
217        let server = FtpServer::new(config);
218
219        let rule = UploadRule {
220            path_pattern: r"^/uploads/.*".to_string(),
221            auto_accept: true,
222            validation: None,
223            storage: UploadStorage::Discard,
224        };
225
226        let fixture = crate::fixtures::FtpFixture {
227            identifier: "test".to_string(),
228            name: "Test".to_string(),
229            description: None,
230            virtual_files: vec![],
231            upload_rules: vec![rule],
232        };
233
234        let new_registry = FtpSpecRegistry::new()
235            .with_vfs(server.vfs.clone())
236            .with_fixtures(vec![fixture])
237            .unwrap();
238
239        let server = FtpServer {
240            config: server.config,
241            vfs: server.vfs.clone(),
242            spec_registry: Arc::new(new_registry),
243        };
244
245        let path = std::path::Path::new("/uploads/test.txt");
246        let data = b"test file content".to_vec();
247
248        let result = server.handle_upload(path, data).await;
249        assert!(result.is_ok());
250
251        // With discard storage, file should not be in VFS
252        let file = server.vfs.get_file_async(path).await;
253        assert!(file.is_none());
254    }
255
256    #[tokio::test]
257    async fn test_handle_upload_validation_failure() {
258        let config = FtpConfig {
259            host: "127.0.0.1".to_string(),
260            port: 2121,
261            virtual_root: std::path::PathBuf::from("/"),
262            ..Default::default()
263        };
264
265        let server = FtpServer::new(config);
266
267        let rule = UploadRule {
268            path_pattern: r"^/uploads/.*".to_string(),
269            auto_accept: true,
270            validation: Some(FileValidation {
271                max_size_bytes: Some(10),
272                allowed_extensions: None,
273                mime_types: None,
274            }),
275            storage: UploadStorage::Memory,
276        };
277
278        let fixture = crate::fixtures::FtpFixture {
279            identifier: "test".to_string(),
280            name: "Test".to_string(),
281            description: None,
282            virtual_files: vec![],
283            upload_rules: vec![rule],
284        };
285
286        let new_registry = FtpSpecRegistry::new()
287            .with_vfs(server.vfs.clone())
288            .with_fixtures(vec![fixture])
289            .unwrap();
290
291        let server = FtpServer {
292            config: server.config,
293            vfs: server.vfs.clone(),
294            spec_registry: Arc::new(new_registry),
295        };
296
297        let path = std::path::Path::new("/uploads/test.txt");
298        let data = b"this is a very large file that exceeds the limit".to_vec();
299
300        let result = server.handle_upload(path, data).await;
301        assert!(result.is_err());
302    }
303
304    #[tokio::test]
305    async fn test_handle_upload_no_matching_rule() {
306        let config = FtpConfig {
307            host: "127.0.0.1".to_string(),
308            port: 2121,
309            virtual_root: std::path::PathBuf::from("/"),
310            ..Default::default()
311        };
312
313        let server = FtpServer::new(config);
314
315        let path = std::path::Path::new("/no-rule/test.txt");
316        let data = b"test content".to_vec();
317
318        let result = server.handle_upload(path, data).await;
319        // Should succeed but do nothing since no rule matches
320        assert!(result.is_ok());
321    }
322}