mockforge_sdk/
builder.rs

1//! Builder for configuring mock servers
2
3use crate::server::MockServer;
4use crate::{Error, Result};
5use mockforge_core::{Config, FailureConfig, LatencyProfile, ProxyConfig, ServerConfig};
6use std::net::TcpListener;
7use std::path::PathBuf;
8
9/// Builder for creating and configuring mock servers
10pub 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    /// Create a new mock server builder
32    #[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    /// Set the HTTP port
50    #[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    /// Automatically discover an available port
58    ///
59    /// By default, uses port 0 which lets the OS assign any available ephemeral port.
60    /// This is atomic and avoids race conditions in parallel tests.
61    ///
62    /// Use `port_range()` if you need ports within a specific range.
63    #[must_use]
64    pub const fn auto_port(mut self) -> Self {
65        self.auto_port = true;
66        self.port = None;
67        self
68    }
69
70    /// Set the port range for automatic port discovery
71    ///
72    /// When set, `auto_port()` will scan this range for an available port
73    /// instead of using OS-assigned ports.
74    ///
75    /// Note: Scanning a range has a small TOCTOU race condition. For parallel
76    /// tests, prefer using `auto_port()` without a range.
77    #[must_use]
78    pub const fn port_range(mut self, start: u16, end: u16) -> Self {
79        self.port_range = Some((start, end));
80        self
81    }
82
83    /// Set the host address
84    pub fn host(mut self, host: impl Into<String>) -> Self {
85        self.host = Some(host.into());
86        self
87    }
88
89    /// Load configuration from a YAML file
90    pub fn config_file(mut self, path: impl Into<PathBuf>) -> Self {
91        self.config_file = Some(path.into());
92        self
93    }
94
95    /// Load routes from an `OpenAPI` specification
96    pub fn openapi_spec(mut self, path: impl Into<PathBuf>) -> Self {
97        self.openapi_spec = Some(path.into());
98        self
99    }
100
101    /// Set the latency profile for simulating network delays
102    #[must_use]
103    pub fn latency(mut self, profile: LatencyProfile) -> Self {
104        self.latency_profile = Some(profile);
105        self
106    }
107
108    /// Enable failure injection with configuration
109    #[must_use]
110    pub fn failures(mut self, config: FailureConfig) -> Self {
111        self.failure_config = Some(config);
112        self
113    }
114
115    /// Enable proxy mode with configuration
116    #[must_use]
117    pub fn proxy(mut self, config: ProxyConfig) -> Self {
118        self.proxy_config = Some(config);
119        self
120    }
121
122    /// Enable admin API
123    #[must_use]
124    pub const fn admin(mut self, enabled: bool) -> Self {
125        self.enable_admin = enabled;
126        self
127    }
128
129    /// Set admin API port
130    #[must_use]
131    pub const fn admin_port(mut self, port: u16) -> Self {
132        self.admin_port = Some(port);
133        self
134    }
135
136    /// Start the mock server
137    pub async fn start(self) -> Result<MockServer> {
138        // Build the configuration
139        let mut config = if let Some(config_file) = self.config_file {
140            mockforge_core::load_config(&config_file)
141                .await
142                .map_err(|e| Error::InvalidConfig(e.to_string()))?
143        } else {
144            ServerConfig::default()
145        };
146
147        // Apply port settings
148        if self.auto_port {
149            if let Some((start, end)) = self.port_range {
150                // User specified a port range - scan for available port
151                let port = find_available_port(start, end)?;
152                config.http.port = port;
153            } else {
154                // No range specified - use port 0 for OS-assigned port
155                // This is atomic and avoids TOCTOU race conditions
156                config.http.port = 0;
157            }
158        } else if let Some(port) = self.port {
159            config.http.port = port;
160        }
161
162        // Apply other builder settings
163        if let Some(host) = self.host {
164            config.http.host = host;
165        }
166        if let Some(spec_path) = self.openapi_spec {
167            config.http.openapi_spec = Some(spec_path.to_string_lossy().to_string());
168        }
169
170        // Create core config
171        let mut core_config = Config::default();
172
173        if let Some(latency) = self.latency_profile {
174            core_config.latency_enabled = true;
175            core_config.default_latency = latency;
176        }
177
178        if let Some(failures) = self.failure_config {
179            core_config.failures_enabled = true;
180            core_config.failure_config = Some(failures);
181        }
182
183        if let Some(proxy) = self.proxy_config {
184            core_config.proxy = Some(proxy);
185        }
186
187        // Create and start the server
188        let mut server = MockServer::from_config(config, core_config).await?;
189        server.start().await?;
190        Ok(server)
191    }
192}
193
194/// Check if a port is available by attempting to bind to it
195///
196/// Note: There is a small race condition (TOCTOU - Time Of Check, Time Of Use)
197/// between checking availability and the actual server binding. In practice,
198/// this is rarely an issue for test environments. For guaranteed port assignment,
199/// consider using `port(0)` to let the OS assign any available port.
200fn is_port_available(port: u16) -> bool {
201    TcpListener::bind(("127.0.0.1", port)).is_ok()
202}
203
204/// Find an available port in the specified range
205///
206/// Scans the port range and returns the first available port.
207/// Returns an error if no ports are available in the range.
208///
209/// # Arguments
210/// * `start` - Starting port number (inclusive)
211/// * `end` - Ending port number (inclusive)
212///
213/// # Errors
214/// Returns `Error::InvalidConfig` if start >= end
215/// Returns `Error::PortDiscoveryFailed` if no ports are available
216fn find_available_port(start: u16, end: u16) -> Result<u16> {
217    // Validate port range
218    if start >= end {
219        return Err(Error::InvalidConfig(format!(
220            "Invalid port range: start ({start}) must be less than end ({end})"
221        )));
222    }
223
224    // Try to find an available port
225    for port in start..=end {
226        if is_port_available(port) {
227            return Ok(port);
228        }
229    }
230
231    Err(Error::PortDiscoveryFailed(format!(
232        "No available ports found in range {start}-{end}"
233    )))
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use std::path::PathBuf;
240
241    #[test]
242    fn test_builder_new() {
243        let builder = MockServerBuilder::new();
244        assert!(builder.port.is_none());
245        assert!(builder.host.is_none());
246        assert!(!builder.enable_admin);
247        assert!(!builder.auto_port);
248    }
249
250    #[test]
251    fn test_builder_default() {
252        let builder = MockServerBuilder::default();
253        assert!(builder.port.is_none());
254        assert!(builder.host.is_none());
255    }
256
257    #[test]
258    fn test_builder_port() {
259        let builder = MockServerBuilder::new().port(8080);
260        assert_eq!(builder.port, Some(8080));
261        assert!(!builder.auto_port);
262    }
263
264    #[test]
265    fn test_builder_auto_port() {
266        let builder = MockServerBuilder::new().auto_port();
267        assert!(builder.auto_port);
268        assert!(builder.port.is_none());
269    }
270
271    #[test]
272    fn test_builder_auto_port_overrides_manual_port() {
273        let builder = MockServerBuilder::new().port(8080).auto_port();
274        assert!(builder.auto_port);
275        assert!(builder.port.is_none());
276    }
277
278    #[test]
279    fn test_builder_manual_port_overrides_auto_port() {
280        let builder = MockServerBuilder::new().auto_port().port(8080);
281        assert!(!builder.auto_port);
282        assert_eq!(builder.port, Some(8080));
283    }
284
285    #[test]
286    fn test_builder_port_range() {
287        let builder = MockServerBuilder::new().port_range(30000, 31000);
288        assert_eq!(builder.port_range, Some((30000, 31000)));
289    }
290
291    #[test]
292    fn test_builder_host() {
293        let builder = MockServerBuilder::new().host("0.0.0.0");
294        assert_eq!(builder.host, Some("0.0.0.0".to_string()));
295    }
296
297    #[test]
298    fn test_builder_config_file() {
299        let builder = MockServerBuilder::new().config_file("/path/to/config.yaml");
300        assert_eq!(builder.config_file, Some(PathBuf::from("/path/to/config.yaml")));
301    }
302
303    #[test]
304    fn test_builder_openapi_spec() {
305        let builder = MockServerBuilder::new().openapi_spec("/path/to/spec.yaml");
306        assert_eq!(builder.openapi_spec, Some(PathBuf::from("/path/to/spec.yaml")));
307    }
308
309    #[test]
310    fn test_builder_latency() {
311        let latency = LatencyProfile::new(100, 0);
312        let builder = MockServerBuilder::new().latency(latency);
313        assert!(builder.latency_profile.is_some());
314    }
315
316    #[test]
317    fn test_builder_failures() {
318        let failures = FailureConfig {
319            global_error_rate: 0.1,
320            default_status_codes: vec![500, 503],
321            ..Default::default()
322        };
323        let builder = MockServerBuilder::new().failures(failures);
324        assert!(builder.failure_config.is_some());
325    }
326
327    #[test]
328    fn test_builder_proxy() {
329        let proxy = ProxyConfig {
330            enabled: true,
331            target_url: Some("http://example.com".to_string()),
332            ..Default::default()
333        };
334        let builder = MockServerBuilder::new().proxy(proxy);
335        assert!(builder.proxy_config.is_some());
336    }
337
338    #[test]
339    fn test_builder_admin() {
340        let builder = MockServerBuilder::new().admin(true);
341        assert!(builder.enable_admin);
342    }
343
344    #[test]
345    fn test_builder_admin_port() {
346        let builder = MockServerBuilder::new().admin_port(9090);
347        assert_eq!(builder.admin_port, Some(9090));
348    }
349
350    #[test]
351    fn test_builder_fluent_chaining() {
352        let latency = LatencyProfile::new(50, 0);
353        let failures = FailureConfig {
354            global_error_rate: 0.05,
355            default_status_codes: vec![500],
356            ..Default::default()
357        };
358
359        let builder = MockServerBuilder::new()
360            .port(8080)
361            .host("localhost")
362            .latency(latency)
363            .failures(failures)
364            .admin(true)
365            .admin_port(9090);
366
367        assert_eq!(builder.port, Some(8080));
368        assert_eq!(builder.host, Some("localhost".to_string()));
369        assert!(builder.latency_profile.is_some());
370        assert!(builder.failure_config.is_some());
371        assert!(builder.enable_admin);
372        assert_eq!(builder.admin_port, Some(9090));
373    }
374
375    #[test]
376    fn test_is_port_available_unbound_port() {
377        // Port 0 should allow binding (OS will assign a port)
378        assert!(is_port_available(0));
379    }
380
381    #[test]
382    fn test_is_port_available_bound_port() {
383        // Bind to a port first
384        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
385        let addr = listener.local_addr().unwrap();
386        let port = addr.port();
387
388        // Now that port should not be available
389        assert!(!is_port_available(port));
390    }
391
392    #[test]
393    fn test_find_available_port_success() {
394        // Should find an available port in a large range
395        let result = find_available_port(30000, 35000);
396        assert!(result.is_ok());
397        let port = result.unwrap();
398        assert!(port >= 30000 && port <= 35000);
399    }
400
401    #[test]
402    fn test_find_available_port_invalid_range_equal() {
403        let result = find_available_port(8080, 8080);
404        assert!(result.is_err());
405        match result {
406            Err(Error::InvalidConfig(msg)) => {
407                assert!(msg.contains("Invalid port range"));
408                assert!(msg.contains("8080"));
409            }
410            _ => panic!("Expected InvalidConfig error"),
411        }
412    }
413
414    #[test]
415    fn test_find_available_port_invalid_range_reversed() {
416        let result = find_available_port(9000, 8000);
417        assert!(result.is_err());
418        match result {
419            Err(Error::InvalidConfig(msg)) => {
420                assert!(msg.contains("Invalid port range"));
421            }
422            _ => panic!("Expected InvalidConfig error"),
423        }
424    }
425
426    #[test]
427    fn test_find_available_port_no_ports_available() {
428        // Bind to all ports in a small range
429        let port1 = 40000;
430        let port2 = 40001;
431        let _listener1 = TcpListener::bind(("127.0.0.1", port1)).ok();
432        let _listener2 = TcpListener::bind(("127.0.0.1", port2)).ok();
433
434        // If both binds succeeded, the search should fail
435        if _listener1.is_some() && _listener2.is_some() {
436            let result = find_available_port(port1, port2);
437            assert!(result.is_err());
438            match result {
439                Err(Error::PortDiscoveryFailed(msg)) => {
440                    assert!(msg.contains("No available ports"));
441                    assert!(msg.contains("40000"));
442                    assert!(msg.contains("40001"));
443                }
444                _ => panic!("Expected PortDiscoveryFailed error"),
445            }
446        }
447    }
448
449    #[test]
450    fn test_find_available_port_single_port_range() {
451        // Even though start < end, this is a valid single-port range (inclusive)
452        let result = find_available_port(45000, 45001);
453        assert!(result.is_ok());
454        let port = result.unwrap();
455        assert!(port == 45000 || port == 45001);
456    }
457
458    #[test]
459    fn test_builder_multiple_config_sources() {
460        let builder = MockServerBuilder::new()
461            .config_file("/path/to/config.yaml")
462            .openapi_spec("/path/to/spec.yaml")
463            .port(8080)
464            .host("localhost");
465
466        assert!(builder.config_file.is_some());
467        assert!(builder.openapi_spec.is_some());
468        assert_eq!(builder.port, Some(8080));
469        assert_eq!(builder.host, Some("localhost".to_string()));
470    }
471
472    #[test]
473    fn test_builder_with_all_features() {
474        let latency = LatencyProfile::new(100, 0);
475        let failures = FailureConfig {
476            global_error_rate: 0.1,
477            default_status_codes: vec![500, 503],
478            ..Default::default()
479        };
480        let proxy = ProxyConfig {
481            enabled: true,
482            target_url: Some("http://backend.com".to_string()),
483            ..Default::default()
484        };
485
486        let builder = MockServerBuilder::new()
487            .port(8080)
488            .host("0.0.0.0")
489            .config_file("/config.yaml")
490            .openapi_spec("/spec.yaml")
491            .latency(latency)
492            .failures(failures)
493            .proxy(proxy)
494            .admin(true)
495            .admin_port(9090);
496
497        assert!(builder.port.is_some());
498        assert!(builder.host.is_some());
499        assert!(builder.config_file.is_some());
500        assert!(builder.openapi_spec.is_some());
501        assert!(builder.latency_profile.is_some());
502        assert!(builder.failure_config.is_some());
503        assert!(builder.proxy_config.is_some());
504        assert!(builder.enable_admin);
505        assert!(builder.admin_port.is_some());
506    }
507
508    #[test]
509    fn test_builder_port_range_default() {
510        let builder = MockServerBuilder::new().auto_port();
511        // Default range should be used if not specified
512        assert!(builder.port_range.is_none());
513    }
514
515    #[test]
516    fn test_builder_port_range_custom() {
517        let builder = MockServerBuilder::new().auto_port().port_range(40000, 50000);
518        assert_eq!(builder.port_range, Some((40000, 50000)));
519    }
520}