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#[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 let storage = MockForgeStorage::new(self.vfs.clone(), self.spec_registry.clone());
35
36 let server = ServerBuilder::new(Box::new(move || storage.clone()))
38 .greeting("MockForge FTP Server")
39 .passive_ports(49152..=65534); 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 let path_str = path.to_string_lossy();
51
52 if let Some(rule) = self.spec_registry.find_upload_rule(&path_str) {
54 rule.validate_file(&data, &path_str).map_err(|e| anyhow::anyhow!(e))?;
56
57 if rule.auto_accept {
58 match &rule.storage {
60 crate::fixtures::UploadStorage::Memory => {
61 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 tokio::fs::write(storage_path, &data).await?;
76 }
77 crate::fixtures::UploadStorage::Discard => {
78 }
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 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 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 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 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 assert!(result.is_ok());
321 }
322}