1use 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
14pub 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 pub fn builder() -> MockForgeServerBuilder {
27 MockForgeServerBuilder::default()
28 }
29
30 pub async fn start(config: ServerConfig) -> Result<Self> {
32 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 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 let health = HealthCheck::new("localhost", http_port);
47
48 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 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 pub fn http_port(&self) -> u16 {
71 self.http_port
72 }
73
74 pub fn ws_port(&self) -> Option<u16> {
76 self.ws_port
77 }
78
79 pub fn grpc_port(&self) -> Option<u16> {
81 self.grpc_port
82 }
83
84 pub fn base_url(&self) -> String {
86 format!("http://localhost:{}", self.http_port)
87 }
88
89 pub fn ws_url(&self) -> Option<String> {
91 self.ws_port.map(|port| format!("ws://localhost:{}/ws", port))
92 }
93
94 pub fn pid(&self) -> u32 {
96 self.process.lock().pid()
97 }
98
99 pub fn is_running(&self) -> bool {
101 self.process.lock().is_running()
102 }
103
104 pub async fn health_check(&self) -> Result<HealthStatus> {
106 self.health.check().await
107 }
108
109 pub async fn is_ready(&self) -> bool {
111 self.health.is_ready().await
112 }
113
114 pub async fn scenario(&self, scenario_name: &str) -> Result<()> {
131 self.scenario.switch_scenario(scenario_name).await
132 }
133
134 pub async fn load_workspace<P: AsRef<Path>>(&self, workspace_file: P) -> Result<()> {
136 self.scenario.load_workspace(workspace_file).await
137 }
138
139 pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
141 self.scenario.update_mock(endpoint, config).await
142 }
143
144 pub async fn list_fixtures(&self) -> Result<Vec<String>> {
146 self.scenario.list_fixtures().await
147 }
148
149 pub async fn get_stats(&self) -> Result<Value> {
151 self.scenario.get_stats().await
152 }
153
154 pub async fn reset(&self) -> Result<()> {
156 self.scenario.reset().await
157 }
158
159 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
174pub 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 pub fn http_port(mut self, port: u16) -> Self {
190 self.config_builder = self.config_builder.http_port(port);
191 self
192 }
193
194 pub fn ws_port(mut self, port: u16) -> Self {
196 self.config_builder = self.config_builder.ws_port(port);
197 self
198 }
199
200 pub fn grpc_port(mut self, port: u16) -> Self {
202 self.config_builder = self.config_builder.grpc_port(port);
203 self
204 }
205
206 pub fn admin_port(mut self, port: u16) -> Self {
208 self.config_builder = self.config_builder.admin_port(port);
209 self
210 }
211
212 pub fn metrics_port(mut self, port: u16) -> Self {
214 self.config_builder = self.config_builder.metrics_port(port);
215 self
216 }
217
218 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 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 pub fn profile(mut self, profile: impl Into<String>) -> Self {
232 self.config_builder = self.config_builder.profile(profile);
233 self
234 }
235
236 pub fn enable_admin(mut self, enable: bool) -> Self {
238 self.config_builder = self.config_builder.enable_admin(enable);
239 self
240 }
241
242 pub fn enable_metrics(mut self, enable: bool) -> Self {
244 self.config_builder = self.config_builder.enable_metrics(enable);
245 self
246 }
247
248 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 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 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 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 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 pub async fn build(self) -> Result<MockForgeServer> {
280 let config = self.config_builder.build();
281 MockForgeServer::start(config).await
282 }
283}
284
285pub 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 assert!(true);
307 }
308}