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