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