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}
21
22impl MockForgeServer {
23    /// Create a new builder for MockForgeServer
24    pub fn builder() -> MockForgeServerBuilder {
25        MockForgeServerBuilder::default()
26    }
27
28    /// Start a MockForge server with the given configuration
29    pub async fn start(config: ServerConfig) -> Result<Self> {
30        // Resolve port (auto-assign if 0)
31        let mut resolved_config = config.clone();
32        if resolved_config.http_port == 0 {
33            resolved_config.http_port = find_available_port(30000)?;
34            info!("Auto-assigned HTTP port: {}", resolved_config.http_port);
35        }
36
37        // Spawn the process
38        let process = ManagedProcess::spawn(&resolved_config)?;
39        let http_port = process.http_port();
40
41        info!("MockForge server started on port {}", http_port);
42
43        // Create health check client
44        let health = HealthCheck::new("localhost", http_port);
45
46        // Wait for server to become healthy
47        debug!("Waiting for server to become healthy...");
48        health
49            .wait_until_healthy(resolved_config.health_timeout, resolved_config.health_interval)
50            .await?;
51
52        info!("MockForge server is healthy and ready");
53
54        // Create scenario manager
55        let scenario = ScenarioManager::new("localhost", http_port);
56
57        Ok(Self {
58            process: Arc::new(Mutex::new(process)),
59            health,
60            scenario,
61            http_port,
62        })
63    }
64
65    /// Get the HTTP port the server is running on
66    pub fn http_port(&self) -> u16 {
67        self.http_port
68    }
69
70    /// Get the base URL of the server
71    pub fn base_url(&self) -> String {
72        format!("http://localhost:{}", self.http_port)
73    }
74
75    /// Get the process ID
76    pub fn pid(&self) -> u32 {
77        self.process.lock().pid()
78    }
79
80    /// Check if the server is still running
81    pub fn is_running(&self) -> bool {
82        self.process.lock().is_running()
83    }
84
85    /// Perform a health check
86    pub async fn health_check(&self) -> Result<HealthStatus> {
87        self.health.check().await
88    }
89
90    /// Check if the server is ready
91    pub async fn is_ready(&self) -> bool {
92        self.health.is_ready().await
93    }
94
95    /// Switch to a different scenario/workspace
96    ///
97    /// # Arguments
98    ///
99    /// * `scenario_name` - Name of the scenario to switch to
100    ///
101    /// # Example
102    ///
103    /// ```no_run
104    /// # use mockforge_test::MockForgeServer;
105    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
106    /// let server = MockForgeServer::builder().build().await?;
107    /// server.scenario("user-auth-success").await?;
108    /// # Ok(())
109    /// # }
110    /// ```
111    pub async fn scenario(&self, scenario_name: &str) -> Result<()> {
112        self.scenario.switch_scenario(scenario_name).await
113    }
114
115    /// Load a workspace configuration from a file
116    pub async fn load_workspace<P: AsRef<Path>>(&self, workspace_file: P) -> Result<()> {
117        self.scenario.load_workspace(workspace_file).await
118    }
119
120    /// Update mock configuration for a specific endpoint
121    pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
122        self.scenario.update_mock(endpoint, config).await
123    }
124
125    /// List available fixtures
126    pub async fn list_fixtures(&self) -> Result<Vec<String>> {
127        self.scenario.list_fixtures().await
128    }
129
130    /// Get server statistics
131    pub async fn get_stats(&self) -> Result<Value> {
132        self.scenario.get_stats().await
133    }
134
135    /// Reset all mocks to their initial state
136    pub async fn reset(&self) -> Result<()> {
137        self.scenario.reset().await
138    }
139
140    /// Stop the server
141    pub fn stop(&self) -> Result<()> {
142        info!("Stopping MockForge server (port: {})", self.http_port);
143        self.process.lock().kill()
144    }
145}
146
147impl Drop for MockForgeServer {
148    fn drop(&mut self) {
149        if let Err(e) = self.stop() {
150            eprintln!("Failed to stop MockForge server on drop: {}", e);
151        }
152    }
153}
154
155/// Builder for MockForgeServer
156pub struct MockForgeServerBuilder {
157    config_builder: ServerConfigBuilder,
158}
159
160impl Default for MockForgeServerBuilder {
161    fn default() -> Self {
162        Self {
163            config_builder: ServerConfig::builder(),
164        }
165    }
166}
167
168impl MockForgeServerBuilder {
169    /// Set HTTP port (0 for auto-assign)
170    pub fn http_port(mut self, port: u16) -> Self {
171        self.config_builder = self.config_builder.http_port(port);
172        self
173    }
174
175    /// Set WebSocket port
176    pub fn ws_port(mut self, port: u16) -> Self {
177        self.config_builder = self.config_builder.ws_port(port);
178        self
179    }
180
181    /// Set gRPC port
182    pub fn grpc_port(mut self, port: u16) -> Self {
183        self.config_builder = self.config_builder.grpc_port(port);
184        self
185    }
186
187    /// Set admin UI port
188    pub fn admin_port(mut self, port: u16) -> Self {
189        self.config_builder = self.config_builder.admin_port(port);
190        self
191    }
192
193    /// Set metrics port
194    pub fn metrics_port(mut self, port: u16) -> Self {
195        self.config_builder = self.config_builder.metrics_port(port);
196        self
197    }
198
199    /// Set OpenAPI specification file
200    pub fn spec_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
201        self.config_builder = self.config_builder.spec_file(path);
202        self
203    }
204
205    /// Set workspace directory
206    pub fn workspace_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
207        self.config_builder = self.config_builder.workspace_dir(path);
208        self
209    }
210
211    /// Set profile name
212    pub fn profile(mut self, profile: impl Into<String>) -> Self {
213        self.config_builder = self.config_builder.profile(profile);
214        self
215    }
216
217    /// Enable admin UI
218    pub fn enable_admin(mut self, enable: bool) -> Self {
219        self.config_builder = self.config_builder.enable_admin(enable);
220        self
221    }
222
223    /// Enable metrics endpoint
224    pub fn enable_metrics(mut self, enable: bool) -> Self {
225        self.config_builder = self.config_builder.enable_metrics(enable);
226        self
227    }
228
229    /// Add extra CLI argument
230    pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
231        self.config_builder = self.config_builder.extra_arg(arg);
232        self
233    }
234
235    /// Set health check timeout
236    pub fn health_timeout(mut self, timeout: std::time::Duration) -> Self {
237        self.config_builder = self.config_builder.health_timeout(timeout);
238        self
239    }
240
241    /// Set working directory
242    pub fn working_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
243        self.config_builder = self.config_builder.working_dir(path);
244        self
245    }
246
247    /// Add environment variable
248    pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
249        self.config_builder = self.config_builder.env_var(key, value);
250        self
251    }
252
253    /// Set path to mockforge binary
254    pub fn binary_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
255        self.config_builder = self.config_builder.binary_path(path);
256        self
257    }
258
259    /// Build and start the MockForge server
260    pub async fn build(self) -> Result<MockForgeServer> {
261        let config = self.config_builder.build();
262        MockForgeServer::start(config).await
263    }
264}
265
266// Helper function for use with test frameworks
267/// Create a test server with default configuration
268pub async fn with_mockforge<F, Fut>(test: F) -> Result<()>
269where
270    F: FnOnce(MockForgeServer) -> Fut,
271    Fut: std::future::Future<Output = Result<()>>,
272{
273    let server = MockForgeServer::builder().build().await?;
274    test(server).await
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_builder_creation() {
283        let _builder =
284            MockForgeServer::builder().http_port(3000).enable_admin(true).profile("test");
285
286        // Builder should compile without errors
287        assert!(true);
288    }
289}