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}
21
22impl MockForgeServer {
23 pub fn builder() -> MockForgeServerBuilder {
25 MockForgeServerBuilder::default()
26 }
27
28 pub async fn start(config: ServerConfig) -> Result<Self> {
30 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 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 let health = HealthCheck::new("localhost", http_port);
45
46 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 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 pub fn http_port(&self) -> u16 {
67 self.http_port
68 }
69
70 pub fn base_url(&self) -> String {
72 format!("http://localhost:{}", self.http_port)
73 }
74
75 pub fn pid(&self) -> u32 {
77 self.process.lock().pid()
78 }
79
80 pub fn is_running(&self) -> bool {
82 self.process.lock().is_running()
83 }
84
85 pub async fn health_check(&self) -> Result<HealthStatus> {
87 self.health.check().await
88 }
89
90 pub async fn is_ready(&self) -> bool {
92 self.health.is_ready().await
93 }
94
95 pub async fn scenario(&self, scenario_name: &str) -> Result<()> {
112 self.scenario.switch_scenario(scenario_name).await
113 }
114
115 pub async fn load_workspace<P: AsRef<Path>>(&self, workspace_file: P) -> Result<()> {
117 self.scenario.load_workspace(workspace_file).await
118 }
119
120 pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
122 self.scenario.update_mock(endpoint, config).await
123 }
124
125 pub async fn list_fixtures(&self) -> Result<Vec<String>> {
127 self.scenario.list_fixtures().await
128 }
129
130 pub async fn get_stats(&self) -> Result<Value> {
132 self.scenario.get_stats().await
133 }
134
135 pub async fn reset(&self) -> Result<()> {
137 self.scenario.reset().await
138 }
139
140 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
155pub 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 pub fn http_port(mut self, port: u16) -> Self {
171 self.config_builder = self.config_builder.http_port(port);
172 self
173 }
174
175 pub fn ws_port(mut self, port: u16) -> Self {
177 self.config_builder = self.config_builder.ws_port(port);
178 self
179 }
180
181 pub fn grpc_port(mut self, port: u16) -> Self {
183 self.config_builder = self.config_builder.grpc_port(port);
184 self
185 }
186
187 pub fn admin_port(mut self, port: u16) -> Self {
189 self.config_builder = self.config_builder.admin_port(port);
190 self
191 }
192
193 pub fn metrics_port(mut self, port: u16) -> Self {
195 self.config_builder = self.config_builder.metrics_port(port);
196 self
197 }
198
199 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 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 pub fn profile(mut self, profile: impl Into<String>) -> Self {
213 self.config_builder = self.config_builder.profile(profile);
214 self
215 }
216
217 pub fn enable_admin(mut self, enable: bool) -> Self {
219 self.config_builder = self.config_builder.enable_admin(enable);
220 self
221 }
222
223 pub fn enable_metrics(mut self, enable: bool) -> Self {
225 self.config_builder = self.config_builder.enable_metrics(enable);
226 self
227 }
228
229 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 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 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 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 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 pub async fn build(self) -> Result<MockForgeServer> {
261 let config = self.config_builder.build();
262 MockForgeServer::start(config).await
263 }
264}
265
266pub 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 assert!(true);
288 }
289}