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    #[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    /// Set the port range for automatic port discovery
66    /// Default range is 30000-30100
67    #[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    /// Set the host address
74    pub fn host(mut self, host: impl Into<String>) -> Self {
75        self.host = Some(host.into());
76        self
77    }
78
79    /// Load configuration from a YAML file
80    pub fn config_file(mut self, path: impl Into<PathBuf>) -> Self {
81        self.config_file = Some(path.into());
82        self
83    }
84
85    /// Load routes from an `OpenAPI` specification
86    pub fn openapi_spec(mut self, path: impl Into<PathBuf>) -> Self {
87        self.openapi_spec = Some(path.into());
88        self
89    }
90
91    /// Set the latency profile for simulating network delays
92    #[must_use]
93    pub fn latency(mut self, profile: LatencyProfile) -> Self {
94        self.latency_profile = Some(profile);
95        self
96    }
97
98    /// Enable failure injection with configuration
99    #[must_use]
100    pub fn failures(mut self, config: FailureConfig) -> Self {
101        self.failure_config = Some(config);
102        self
103    }
104
105    /// Enable proxy mode with configuration
106    #[must_use]
107    pub fn proxy(mut self, config: ProxyConfig) -> Self {
108        self.proxy_config = Some(config);
109        self
110    }
111
112    /// Enable admin API
113    #[must_use]
114    pub const fn admin(mut self, enabled: bool) -> Self {
115        self.enable_admin = enabled;
116        self
117    }
118
119    /// Set admin API port
120    #[must_use]
121    pub const fn admin_port(mut self, port: u16) -> Self {
122        self.admin_port = Some(port);
123        self
124    }
125
126    /// Start the mock server
127    pub async fn start(self) -> Result<MockServer> {
128        // Build the configuration
129        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        // Apply port settings
138        if self.auto_port {
139            // Discover an available port
140            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        // Apply other builder settings
148        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        // Create core config
156        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        // Create and start the server
173        MockServer::from_config(config, core_config).await
174    }
175}
176
177/// Check if a port is available by attempting to bind to it
178///
179/// Note: There is a small race condition (TOCTOU - Time Of Check, Time Of Use)
180/// between checking availability and the actual server binding. In practice,
181/// this is rarely an issue for test environments. For guaranteed port assignment,
182/// consider using `port(0)` to let the OS assign any available port.
183fn is_port_available(port: u16) -> bool {
184    TcpListener::bind(("127.0.0.1", port)).is_ok()
185}
186
187/// Find an available port in the specified range
188///
189/// Scans the port range and returns the first available port.
190/// Returns an error if no ports are available in the range.
191///
192/// # Arguments
193/// * `start` - Starting port number (inclusive)
194/// * `end` - Ending port number (inclusive)
195///
196/// # Errors
197/// Returns `Error::InvalidConfig` if start >= end
198/// Returns `Error::PortDiscoveryFailed` if no ports are available
199fn find_available_port(start: u16, end: u16) -> Result<u16> {
200    // Validate port range
201    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    // Try to find an available port
208    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        // Port 0 should allow binding (OS will assign a port)
361        assert!(is_port_available(0));
362    }
363
364    #[test]
365    fn test_is_port_available_bound_port() {
366        // Bind to a port first
367        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
368        let addr = listener.local_addr().unwrap();
369        let port = addr.port();
370
371        // Now that port should not be available
372        assert!(!is_port_available(port));
373    }
374
375    #[test]
376    fn test_find_available_port_success() {
377        // Should find an available port in a large range
378        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        // Bind to all ports in a small range
412        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 both binds succeeded, the search should fail
418        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        // Even though start < end, this is a valid single-port range (inclusive)
435        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        // Default range should be used if not specified
495        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}