Skip to main content

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, error, 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            error!("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 = MockForgeServer::builder().http_port(3000).enable_admin(true).profile("test");
304        // Verify builder captured the configuration
305        drop(builder);
306    }
307
308    #[test]
309    fn test_builder_default() {
310        let _builder = MockForgeServerBuilder::default();
311        // Default builder should be created
312    }
313
314    #[test]
315    fn test_builder_http_port() {
316        let builder = MockForgeServer::builder().http_port(8080);
317        // Chain continues
318        let _builder = builder.http_port(9090);
319    }
320
321    #[test]
322    fn test_builder_ws_port() {
323        let _builder = MockForgeServer::builder().ws_port(3001);
324    }
325
326    #[test]
327    fn test_builder_grpc_port() {
328        let _builder = MockForgeServer::builder().grpc_port(50051);
329    }
330
331    #[test]
332    fn test_builder_admin_port() {
333        let _builder = MockForgeServer::builder().admin_port(3002);
334    }
335
336    #[test]
337    fn test_builder_metrics_port() {
338        let _builder = MockForgeServer::builder().metrics_port(9090);
339    }
340
341    #[test]
342    fn test_builder_spec_file() {
343        let _builder = MockForgeServer::builder().spec_file("/path/to/spec.yaml");
344    }
345
346    #[test]
347    fn test_builder_workspace_dir() {
348        let _builder = MockForgeServer::builder().workspace_dir("/path/to/workspace");
349    }
350
351    #[test]
352    fn test_builder_profile() {
353        let _builder = MockForgeServer::builder().profile("production");
354    }
355
356    #[test]
357    fn test_builder_enable_admin() {
358        let _builder = MockForgeServer::builder().enable_admin(true);
359        let _builder2 = MockForgeServer::builder().enable_admin(false);
360    }
361
362    #[test]
363    fn test_builder_enable_metrics() {
364        let _builder = MockForgeServer::builder().enable_metrics(true);
365        let _builder2 = MockForgeServer::builder().enable_metrics(false);
366    }
367
368    #[test]
369    fn test_builder_extra_arg() {
370        let _builder = MockForgeServer::builder().extra_arg("--verbose");
371    }
372
373    #[test]
374    fn test_builder_health_timeout() {
375        let _builder = MockForgeServer::builder().health_timeout(Duration::from_secs(60));
376    }
377
378    #[test]
379    fn test_builder_working_dir() {
380        let _builder = MockForgeServer::builder().working_dir("/tmp/test");
381    }
382
383    #[test]
384    fn test_builder_env_var() {
385        let _builder = MockForgeServer::builder().env_var("RUST_LOG", "debug");
386    }
387
388    #[test]
389    fn test_builder_binary_path() {
390        let _builder = MockForgeServer::builder().binary_path("/usr/local/bin/mockforge");
391    }
392
393    #[test]
394    fn test_builder_full_chain() {
395        let _builder = MockForgeServer::builder()
396            .http_port(3000)
397            .ws_port(3001)
398            .grpc_port(50051)
399            .admin_port(3002)
400            .metrics_port(9090)
401            .spec_file("/spec.yaml")
402            .workspace_dir("/workspace")
403            .profile("test")
404            .enable_admin(true)
405            .enable_metrics(true)
406            .extra_arg("--verbose")
407            .health_timeout(Duration::from_secs(30))
408            .working_dir("/working")
409            .env_var("KEY", "VALUE")
410            .binary_path("/bin/mockforge");
411
412        // Full builder chain should compile
413    }
414}