1#![allow(clippy::missing_errors_doc)]
3
4use crate::server::MockServer;
5use crate::{Error, Result};
6use mockforge_core::{Config, ProxyConfig, ServerConfig};
7use mockforge_foundation::failure_injection::FailureConfig;
8use mockforge_foundation::latency::LatencyProfile;
9use std::net::TcpListener;
10use std::path::PathBuf;
11
12pub struct MockServerBuilder {
14 port: Option<u16>,
15 host: Option<String>,
16 config_file: Option<PathBuf>,
17 openapi_spec: Option<PathBuf>,
18 latency_profile: Option<LatencyProfile>,
19 failure_config: Option<FailureConfig>,
20 proxy_config: Option<ProxyConfig>,
21 enable_admin: bool,
22 admin_port: Option<u16>,
23 auto_port: bool,
24 port_range: Option<(u16, u16)>,
25}
26
27impl Default for MockServerBuilder {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl MockServerBuilder {
34 #[must_use]
36 pub const fn new() -> Self {
37 Self {
38 port: None,
39 host: None,
40 config_file: None,
41 openapi_spec: None,
42 latency_profile: None,
43 failure_config: None,
44 proxy_config: None,
45 enable_admin: false,
46 admin_port: None,
47 auto_port: false,
48 port_range: None,
49 }
50 }
51
52 #[must_use]
54 pub const fn port(mut self, port: u16) -> Self {
55 self.port = Some(port);
56 self.auto_port = false;
57 self
58 }
59
60 #[must_use]
67 pub const fn auto_port(mut self) -> Self {
68 self.auto_port = true;
69 self.port = None;
70 self
71 }
72
73 #[must_use]
81 pub const fn port_range(mut self, start: u16, end: u16) -> Self {
82 self.port_range = Some((start, end));
83 self
84 }
85
86 #[must_use]
88 pub fn host(mut self, host: impl Into<String>) -> Self {
89 self.host = Some(host.into());
90 self
91 }
92
93 #[must_use]
95 pub fn config_file(mut self, path: impl Into<PathBuf>) -> Self {
96 self.config_file = Some(path.into());
97 self
98 }
99
100 #[must_use]
102 pub fn openapi_spec(mut self, path: impl Into<PathBuf>) -> Self {
103 self.openapi_spec = Some(path.into());
104 self
105 }
106
107 #[must_use]
109 pub fn latency(mut self, profile: LatencyProfile) -> Self {
110 self.latency_profile = Some(profile);
111 self
112 }
113
114 #[must_use]
116 pub fn failures(mut self, config: FailureConfig) -> Self {
117 self.failure_config = Some(config);
118 self
119 }
120
121 #[must_use]
123 pub fn proxy(mut self, config: ProxyConfig) -> Self {
124 self.proxy_config = Some(config);
125 self
126 }
127
128 #[must_use]
130 pub const fn admin(mut self, enabled: bool) -> Self {
131 self.enable_admin = enabled;
132 self
133 }
134
135 #[must_use]
137 pub const fn admin_port(mut self, port: u16) -> Self {
138 self.admin_port = Some(port);
139 self
140 }
141
142 pub async fn start(self) -> Result<MockServer> {
144 let mut config = if let Some(config_file) = self.config_file {
146 mockforge_core::load_config(&config_file)
147 .await
148 .map_err(|e| Error::InvalidConfig(e.to_string()))?
149 } else {
150 ServerConfig::default()
151 };
152
153 if self.auto_port {
155 if let Some((start, end)) = self.port_range {
156 let port = find_available_port(start, end)?;
158 config.http.port = port;
159 } else {
160 config.http.port = 0;
163 }
164 } else if let Some(port) = self.port {
165 config.http.port = port;
166 }
167
168 if let Some(host) = self.host {
170 config.http.host = host;
171 }
172 if let Some(spec_path) = self.openapi_spec {
173 config.http.openapi_spec = Some(spec_path.to_string_lossy().to_string());
174 }
175
176 let mut core_config = Config::default();
178
179 if let Some(latency) = self.latency_profile {
180 core_config.latency_enabled = true;
181 core_config.default_latency = latency;
182 }
183
184 if let Some(failures) = self.failure_config {
185 core_config.failures_enabled = true;
186 core_config.failure_config = Some(failures);
187 }
188
189 if let Some(proxy) = self.proxy_config {
190 core_config.proxy = Some(proxy);
191 }
192
193 let mut server = MockServer::from_config(config, core_config)?;
195 server.start().await?;
196 Ok(server)
197 }
198}
199
200fn is_port_available(port: u16) -> bool {
207 TcpListener::bind(("127.0.0.1", port)).is_ok()
208}
209
210fn find_available_port(start: u16, end: u16) -> Result<u16> {
223 if start >= end {
225 return Err(Error::InvalidConfig(format!(
226 "Invalid port range: start ({start}) must be less than end ({end})"
227 )));
228 }
229
230 for port in start..=end {
232 if is_port_available(port) {
233 return Ok(port);
234 }
235 }
236
237 Err(Error::PortDiscoveryFailed(format!(
238 "No available ports found in range {start}-{end}"
239 )))
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use std::path::PathBuf;
246
247 #[test]
248 fn test_builder_new() {
249 let builder = MockServerBuilder::new();
250 assert!(builder.port.is_none());
251 assert!(builder.host.is_none());
252 assert!(!builder.enable_admin);
253 assert!(!builder.auto_port);
254 }
255
256 #[test]
257 fn test_builder_default() {
258 let builder = MockServerBuilder::default();
259 assert!(builder.port.is_none());
260 assert!(builder.host.is_none());
261 }
262
263 #[test]
264 fn test_builder_port() {
265 let builder = MockServerBuilder::new().port(8080);
266 assert_eq!(builder.port, Some(8080));
267 assert!(!builder.auto_port);
268 }
269
270 #[test]
271 fn test_builder_auto_port() {
272 let builder = MockServerBuilder::new().auto_port();
273 assert!(builder.auto_port);
274 assert!(builder.port.is_none());
275 }
276
277 #[test]
278 fn test_builder_auto_port_overrides_manual_port() {
279 let builder = MockServerBuilder::new().port(8080).auto_port();
280 assert!(builder.auto_port);
281 assert!(builder.port.is_none());
282 }
283
284 #[test]
285 fn test_builder_manual_port_overrides_auto_port() {
286 let builder = MockServerBuilder::new().auto_port().port(8080);
287 assert!(!builder.auto_port);
288 assert_eq!(builder.port, Some(8080));
289 }
290
291 #[test]
292 fn test_builder_port_range() {
293 let builder = MockServerBuilder::new().port_range(30000, 31000);
294 assert_eq!(builder.port_range, Some((30000, 31000)));
295 }
296
297 #[test]
298 fn test_builder_host() {
299 let builder = MockServerBuilder::new().host("0.0.0.0");
300 assert_eq!(builder.host, Some("0.0.0.0".to_string()));
301 }
302
303 #[test]
304 fn test_builder_config_file() {
305 let builder = MockServerBuilder::new().config_file("/path/to/config.yaml");
306 assert_eq!(builder.config_file, Some(PathBuf::from("/path/to/config.yaml")));
307 }
308
309 #[test]
310 fn test_builder_openapi_spec() {
311 let builder = MockServerBuilder::new().openapi_spec("/path/to/spec.yaml");
312 assert_eq!(builder.openapi_spec, Some(PathBuf::from("/path/to/spec.yaml")));
313 }
314
315 #[test]
316 fn test_builder_latency() {
317 let latency = LatencyProfile::new(100, 0);
318 let builder = MockServerBuilder::new().latency(latency);
319 assert!(builder.latency_profile.is_some());
320 }
321
322 #[test]
323 fn test_builder_failures() {
324 let failures = FailureConfig {
325 global_error_rate: 0.1,
326 default_status_codes: vec![500, 503],
327 ..Default::default()
328 };
329 let builder = MockServerBuilder::new().failures(failures);
330 assert!(builder.failure_config.is_some());
331 }
332
333 #[test]
334 fn test_builder_proxy() {
335 let proxy = ProxyConfig {
336 enabled: true,
337 target_url: Some("http://example.com".to_string()),
338 ..Default::default()
339 };
340 let builder = MockServerBuilder::new().proxy(proxy);
341 assert!(builder.proxy_config.is_some());
342 }
343
344 #[test]
345 fn test_builder_admin() {
346 let builder = MockServerBuilder::new().admin(true);
347 assert!(builder.enable_admin);
348 }
349
350 #[test]
351 fn test_builder_admin_port() {
352 let builder = MockServerBuilder::new().admin_port(9090);
353 assert_eq!(builder.admin_port, Some(9090));
354 }
355
356 #[test]
357 fn test_builder_fluent_chaining() {
358 let latency = LatencyProfile::new(50, 0);
359 let failures = FailureConfig {
360 global_error_rate: 0.05,
361 default_status_codes: vec![500],
362 ..Default::default()
363 };
364
365 let builder = MockServerBuilder::new()
366 .port(8080)
367 .host("localhost")
368 .latency(latency)
369 .failures(failures)
370 .admin(true)
371 .admin_port(9090);
372
373 assert_eq!(builder.port, Some(8080));
374 assert_eq!(builder.host, Some("localhost".to_string()));
375 assert!(builder.latency_profile.is_some());
376 assert!(builder.failure_config.is_some());
377 assert!(builder.enable_admin);
378 assert_eq!(builder.admin_port, Some(9090));
379 }
380
381 #[test]
382 fn test_is_port_available_unbound_port() {
383 assert!(is_port_available(0));
385 }
386
387 #[test]
388 fn test_is_port_available_bound_port() {
389 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
391 let addr = listener.local_addr().unwrap();
392 let port = addr.port();
393
394 assert!(!is_port_available(port));
396 }
397
398 #[test]
399 fn test_find_available_port_success() {
400 let result = find_available_port(30000, 35000);
402 assert!(result.is_ok());
403 let port = result.unwrap();
404 assert!((30000..=35000).contains(&port));
405 }
406
407 #[test]
408 fn test_find_available_port_invalid_range_equal() {
409 let result = find_available_port(8080, 8080);
410 assert!(result.is_err());
411 match result {
412 Err(Error::InvalidConfig(msg)) => {
413 assert!(msg.contains("Invalid port range"));
414 assert!(msg.contains("8080"));
415 }
416 _ => panic!("Expected InvalidConfig error"),
417 }
418 }
419
420 #[test]
421 fn test_find_available_port_invalid_range_reversed() {
422 let result = find_available_port(9000, 8000);
423 assert!(result.is_err());
424 match result {
425 Err(Error::InvalidConfig(msg)) => {
426 assert!(msg.contains("Invalid port range"));
427 }
428 _ => panic!("Expected InvalidConfig error"),
429 }
430 }
431
432 #[test]
433 fn test_find_available_port_no_ports_available() {
434 let port1 = 40000;
436 let port2 = 40001;
437 let listener1 = TcpListener::bind(("127.0.0.1", port1)).ok();
438 let listener2 = TcpListener::bind(("127.0.0.1", port2)).ok();
439
440 if listener1.is_some() && listener2.is_some() {
442 let result = find_available_port(port1, port2);
443 assert!(result.is_err());
444 match result {
445 Err(Error::PortDiscoveryFailed(msg)) => {
446 assert!(msg.contains("No available ports"));
447 assert!(msg.contains("40000"));
448 assert!(msg.contains("40001"));
449 }
450 _ => panic!("Expected PortDiscoveryFailed error"),
451 }
452 }
453 }
454
455 #[test]
456 fn test_find_available_port_single_port_range() {
457 let result = find_available_port(45000, 45001);
459 assert!(result.is_ok());
460 let port = result.unwrap();
461 assert!(port == 45000 || port == 45001);
462 }
463
464 #[test]
465 fn test_builder_multiple_config_sources() {
466 let builder = MockServerBuilder::new()
467 .config_file("/path/to/config.yaml")
468 .openapi_spec("/path/to/spec.yaml")
469 .port(8080)
470 .host("localhost");
471
472 assert!(builder.config_file.is_some());
473 assert!(builder.openapi_spec.is_some());
474 assert_eq!(builder.port, Some(8080));
475 assert_eq!(builder.host, Some("localhost".to_string()));
476 }
477
478 #[test]
479 fn test_builder_with_all_features() {
480 let latency = LatencyProfile::new(100, 0);
481 let failures = FailureConfig {
482 global_error_rate: 0.1,
483 default_status_codes: vec![500, 503],
484 ..Default::default()
485 };
486 let proxy = ProxyConfig {
487 enabled: true,
488 target_url: Some("http://backend.com".to_string()),
489 ..Default::default()
490 };
491
492 let builder = MockServerBuilder::new()
493 .port(8080)
494 .host("0.0.0.0")
495 .config_file("/config.yaml")
496 .openapi_spec("/spec.yaml")
497 .latency(latency)
498 .failures(failures)
499 .proxy(proxy)
500 .admin(true)
501 .admin_port(9090);
502
503 assert!(builder.port.is_some());
504 assert!(builder.host.is_some());
505 assert!(builder.config_file.is_some());
506 assert!(builder.openapi_spec.is_some());
507 assert!(builder.latency_profile.is_some());
508 assert!(builder.failure_config.is_some());
509 assert!(builder.proxy_config.is_some());
510 assert!(builder.enable_admin);
511 assert!(builder.admin_port.is_some());
512 }
513
514 #[test]
515 fn test_builder_port_range_default() {
516 let builder = MockServerBuilder::new().auto_port();
517 assert!(builder.port_range.is_none());
519 }
520
521 #[test]
522 fn test_builder_port_range_custom() {
523 let builder = MockServerBuilder::new().auto_port().port_range(40000, 50000);
524 assert_eq!(builder.port_range, Some((40000, 50000)));
525 }
526}