mockforge_test/
server.rs

1//! MockForge server management for tests
2
3use crate::config::{ServerConfig, ServerConfigBuilder};
4use crate::error::Result;
5use crate::health::{HealthCheck, HealthStatus};
6use crate::process::{find_available_port, ManagedProcess};
7use crate::scenario::ScenarioManager;
8use parking_lot::Mutex;
9use serde_json::Value;
10use std::path::Path;
11use std::sync::Arc;
12use tracing::{debug, info};
13
14/// A managed MockForge server instance for testing
15pub struct MockForgeServer {
16    process: Arc<Mutex<ManagedProcess>>,
17    health: HealthCheck,
18    scenario: ScenarioManager,
19    http_port: u16,
20    ws_port: Option<u16>,
21    grpc_port: Option<u16>,
22}
23
24impl MockForgeServer {
25    /// Create a new builder for MockForgeServer
26    pub fn builder() -> MockForgeServerBuilder {
27        MockForgeServerBuilder::default()
28    }
29
30    /// Start a MockForge server with the given configuration
31    pub async fn start(config: ServerConfig) -> Result<Self> {
32        // Resolve port (auto-assign if 0)
33        let mut resolved_config = config.clone();
34        if resolved_config.http_port == 0 {
35            resolved_config.http_port = find_available_port(30000)?;
36            info!("Auto-assigned HTTP port: {}", resolved_config.http_port);
37        }
38
39        // Spawn the process
40        let process = ManagedProcess::spawn(&resolved_config)?;
41        let http_port = process.http_port();
42
43        info!("MockForge server started on port {}", http_port);
44
45        // Create health check client
46        let health = HealthCheck::new("localhost", http_port);
47
48        // Wait for server to become healthy
49        debug!("Waiting for server to become healthy...");
50        health
51            .wait_until_healthy(resolved_config.health_timeout, resolved_config.health_interval)
52            .await?;
53
54        info!("MockForge server is healthy and ready");
55
56        // Create scenario manager
57        let scenario = ScenarioManager::new("localhost", http_port);
58
59        Ok(Self {
60            process: Arc::new(Mutex::new(process)),
61            health,
62            scenario,
63            http_port,
64            ws_port: resolved_config.ws_port,
65            grpc_port: resolved_config.grpc_port,
66        })
67    }
68
69    /// Get the HTTP port the server is running on
70    pub fn http_port(&self) -> u16 {
71        self.http_port
72    }
73
74    /// Get the WebSocket port if configured
75    pub fn ws_port(&self) -> Option<u16> {
76        self.ws_port
77    }
78
79    /// Get the gRPC port if configured
80    pub fn grpc_port(&self) -> Option<u16> {
81        self.grpc_port
82    }
83
84    /// Get the base URL of the server
85    pub fn base_url(&self) -> String {
86        format!("http://localhost:{}", self.http_port)
87    }
88
89    /// Get the WebSocket URL if WebSocket is enabled
90    pub fn ws_url(&self) -> Option<String> {
91        self.ws_port.map(|port| format!("ws://localhost:{}/ws", port))
92    }
93
94    /// Get the process ID
95    pub fn pid(&self) -> u32 {
96        self.process.lock().pid()
97    }
98
99    /// Check if the server is still running
100    pub fn is_running(&self) -> bool {
101        self.process.lock().is_running()
102    }
103
104    /// Perform a health check
105    pub async fn health_check(&self) -> Result<HealthStatus> {
106        self.health.check().await
107    }
108
109    /// Check if the server is ready
110    pub async fn is_ready(&self) -> bool {
111        self.health.is_ready().await
112    }
113
114    /// Switch to a different scenario/workspace
115    ///
116    /// # Arguments
117    ///
118    /// * `scenario_name` - Name of the scenario to switch to
119    ///
120    /// # Example
121    ///
122    /// ```no_run
123    /// # use mockforge_test::MockForgeServer;
124    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
125    /// let server = MockForgeServer::builder().build().await?;
126    /// server.scenario("user-auth-success").await?;
127    /// # Ok(())
128    /// # }
129    /// ```
130    pub async fn scenario(&self, scenario_name: &str) -> Result<()> {
131        self.scenario.switch_scenario(scenario_name).await
132    }
133
134    /// Load a workspace configuration from a file
135    pub async fn load_workspace<P: AsRef<Path>>(&self, workspace_file: P) -> Result<()> {
136        self.scenario.load_workspace(workspace_file).await
137    }
138
139    /// Update mock configuration for a specific endpoint
140    pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
141        self.scenario.update_mock(endpoint, config).await
142    }
143
144    /// List available fixtures
145    pub async fn list_fixtures(&self) -> Result<Vec<String>> {
146        self.scenario.list_fixtures().await
147    }
148
149    /// Get server statistics
150    pub async fn get_stats(&self) -> Result<Value> {
151        self.scenario.get_stats().await
152    }
153
154    /// Reset all mocks to their initial state
155    pub async fn reset(&self) -> Result<()> {
156        self.scenario.reset().await
157    }
158
159    /// Stop the server
160    pub fn stop(&self) -> Result<()> {
161        info!("Stopping MockForge server (port: {})", self.http_port);
162        self.process.lock().kill()
163    }
164}
165
166impl Drop for MockForgeServer {
167    fn drop(&mut self) {
168        if let Err(e) = self.stop() {
169            eprintln!("Failed to stop MockForge server on drop: {}", e);
170        }
171    }
172}
173
174/// Builder for MockForgeServer
175pub struct MockForgeServerBuilder {
176    config_builder: ServerConfigBuilder,
177}
178
179impl Default for MockForgeServerBuilder {
180    fn default() -> Self {
181        Self {
182            config_builder: ServerConfig::builder(),
183        }
184    }
185}
186
187impl MockForgeServerBuilder {
188    /// Set HTTP port (0 for auto-assign)
189    pub fn http_port(mut self, port: u16) -> Self {
190        self.config_builder = self.config_builder.http_port(port);
191        self
192    }
193
194    /// Set WebSocket port
195    pub fn ws_port(mut self, port: u16) -> Self {
196        self.config_builder = self.config_builder.ws_port(port);
197        self
198    }
199
200    /// Set gRPC port
201    pub fn grpc_port(mut self, port: u16) -> Self {
202        self.config_builder = self.config_builder.grpc_port(port);
203        self
204    }
205
206    /// Set admin UI port
207    pub fn admin_port(mut self, port: u16) -> Self {
208        self.config_builder = self.config_builder.admin_port(port);
209        self
210    }
211
212    /// Set metrics port
213    pub fn metrics_port(mut self, port: u16) -> Self {
214        self.config_builder = self.config_builder.metrics_port(port);
215        self
216    }
217
218    /// Set OpenAPI specification file
219    pub fn spec_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
220        self.config_builder = self.config_builder.spec_file(path);
221        self
222    }
223
224    /// Set workspace directory
225    pub fn workspace_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
226        self.config_builder = self.config_builder.workspace_dir(path);
227        self
228    }
229
230    /// Set profile name
231    pub fn profile(mut self, profile: impl Into<String>) -> Self {
232        self.config_builder = self.config_builder.profile(profile);
233        self
234    }
235
236    /// Enable admin UI
237    pub fn enable_admin(mut self, enable: bool) -> Self {
238        self.config_builder = self.config_builder.enable_admin(enable);
239        self
240    }
241
242    /// Enable metrics endpoint
243    pub fn enable_metrics(mut self, enable: bool) -> Self {
244        self.config_builder = self.config_builder.enable_metrics(enable);
245        self
246    }
247
248    /// Add extra CLI argument
249    pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
250        self.config_builder = self.config_builder.extra_arg(arg);
251        self
252    }
253
254    /// Set health check timeout
255    pub fn health_timeout(mut self, timeout: std::time::Duration) -> Self {
256        self.config_builder = self.config_builder.health_timeout(timeout);
257        self
258    }
259
260    /// Set working directory
261    pub fn working_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
262        self.config_builder = self.config_builder.working_dir(path);
263        self
264    }
265
266    /// Add environment variable
267    pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
268        self.config_builder = self.config_builder.env_var(key, value);
269        self
270    }
271
272    /// Set path to mockforge binary
273    pub fn binary_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
274        self.config_builder = self.config_builder.binary_path(path);
275        self
276    }
277
278    /// Build and start the MockForge server
279    pub async fn build(self) -> Result<MockForgeServer> {
280        let config = self.config_builder.build();
281        MockForgeServer::start(config).await
282    }
283}
284
285// Helper function for use with test frameworks
286/// Create a test server with default configuration
287pub async fn with_mockforge<F, Fut>(test: F) -> Result<()>
288where
289    F: FnOnce(MockForgeServer) -> Fut,
290    Fut: std::future::Future<Output = Result<()>>,
291{
292    let server = MockForgeServer::builder().build().await?;
293    test(server).await
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use std::time::Duration;
300
301    #[test]
302    fn test_builder_creation() {
303        let _builder =
304            MockForgeServer::builder().http_port(3000).enable_admin(true).profile("test");
305
306        // Builder should compile without errors
307        assert!(true);
308    }
309
310    #[test]
311    fn test_builder_default() {
312        let _builder = MockForgeServerBuilder::default();
313        // Default builder should be created
314    }
315
316    #[test]
317    fn test_builder_http_port() {
318        let builder = MockForgeServer::builder().http_port(8080);
319        // Chain continues
320        let _builder = builder.http_port(9090);
321    }
322
323    #[test]
324    fn test_builder_ws_port() {
325        let _builder = MockForgeServer::builder().ws_port(3001);
326    }
327
328    #[test]
329    fn test_builder_grpc_port() {
330        let _builder = MockForgeServer::builder().grpc_port(50051);
331    }
332
333    #[test]
334    fn test_builder_admin_port() {
335        let _builder = MockForgeServer::builder().admin_port(3002);
336    }
337
338    #[test]
339    fn test_builder_metrics_port() {
340        let _builder = MockForgeServer::builder().metrics_port(9090);
341    }
342
343    #[test]
344    fn test_builder_spec_file() {
345        let _builder = MockForgeServer::builder().spec_file("/path/to/spec.yaml");
346    }
347
348    #[test]
349    fn test_builder_workspace_dir() {
350        let _builder = MockForgeServer::builder().workspace_dir("/path/to/workspace");
351    }
352
353    #[test]
354    fn test_builder_profile() {
355        let _builder = MockForgeServer::builder().profile("production");
356    }
357
358    #[test]
359    fn test_builder_enable_admin() {
360        let _builder = MockForgeServer::builder().enable_admin(true);
361        let _builder2 = MockForgeServer::builder().enable_admin(false);
362    }
363
364    #[test]
365    fn test_builder_enable_metrics() {
366        let _builder = MockForgeServer::builder().enable_metrics(true);
367        let _builder2 = MockForgeServer::builder().enable_metrics(false);
368    }
369
370    #[test]
371    fn test_builder_extra_arg() {
372        let _builder = MockForgeServer::builder().extra_arg("--verbose");
373    }
374
375    #[test]
376    fn test_builder_health_timeout() {
377        let _builder = MockForgeServer::builder().health_timeout(Duration::from_secs(60));
378    }
379
380    #[test]
381    fn test_builder_working_dir() {
382        let _builder = MockForgeServer::builder().working_dir("/tmp/test");
383    }
384
385    #[test]
386    fn test_builder_env_var() {
387        let _builder = MockForgeServer::builder().env_var("RUST_LOG", "debug");
388    }
389
390    #[test]
391    fn test_builder_binary_path() {
392        let _builder = MockForgeServer::builder().binary_path("/usr/local/bin/mockforge");
393    }
394
395    #[test]
396    fn test_builder_full_chain() {
397        let _builder = MockForgeServer::builder()
398            .http_port(3000)
399            .ws_port(3001)
400            .grpc_port(50051)
401            .admin_port(3002)
402            .metrics_port(9090)
403            .spec_file("/spec.yaml")
404            .workspace_dir("/workspace")
405            .profile("test")
406            .enable_admin(true)
407            .enable_metrics(true)
408            .extra_arg("--verbose")
409            .health_timeout(Duration::from_secs(30))
410            .working_dir("/working")
411            .env_var("KEY", "VALUE")
412            .binary_path("/bin/mockforge");
413
414        // Full builder chain should compile
415    }
416}