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
300    #[test]
301    fn test_builder_creation() {
302        let _builder =
303            MockForgeServer::builder().http_port(3000).enable_admin(true).profile("test");
304
305        // Builder should compile without errors
306        assert!(true);
307    }
308}