Skip to main content

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