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