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, error, 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 admin_port: Option<u16>,
23 metrics_port: Option<u16>,
24}
25
26impl MockForgeServer {
27 pub fn builder() -> MockForgeServerBuilder {
29 MockForgeServerBuilder::default()
30 }
31
32 pub async fn start(config: ServerConfig) -> Result<Self> {
34 let mut resolved_config = config.clone();
40 if resolved_config.http_port == 0 {
41 resolved_config.http_port = find_available_port(30000)?;
42 info!("Auto-assigned HTTP port: {}", resolved_config.http_port);
43 }
44 if resolved_config.admin_port == Some(0) {
45 let port = find_available_port(31000)?;
46 resolved_config.admin_port = Some(port);
47 info!("Auto-assigned admin port: {}", port);
48 }
49 if resolved_config.metrics_port == Some(0) {
50 let port = find_available_port(32000)?;
51 resolved_config.metrics_port = Some(port);
52 info!("Auto-assigned metrics port: {}", port);
53 }
54 if resolved_config.ws_port.is_none() || resolved_config.ws_port == Some(0) {
60 let port = find_available_port(33000)?;
61 resolved_config.ws_port = Some(port);
62 info!("Auto-assigned WebSocket port: {}", port);
63 }
64 if resolved_config.grpc_port.is_none() || resolved_config.grpc_port == Some(0) {
65 let port = find_available_port(34000)?;
66 resolved_config.grpc_port = Some(port);
67 info!("Auto-assigned gRPC port: {}", port);
68 }
69
70 let process = ManagedProcess::spawn(&resolved_config)?;
72 let http_port = process.http_port();
73
74 info!("MockForge server started on port {}", http_port);
75
76 let health = HealthCheck::new("localhost", http_port);
78
79 debug!("Waiting for server to become healthy...");
81 health
82 .wait_until_healthy(resolved_config.health_timeout, resolved_config.health_interval)
83 .await?;
84
85 info!("MockForge server is healthy and ready");
86
87 let scenario = ScenarioManager::new("localhost", http_port);
89
90 Ok(Self {
91 process: Arc::new(Mutex::new(process)),
92 health,
93 scenario,
94 http_port,
95 ws_port: resolved_config.ws_port,
96 grpc_port: resolved_config.grpc_port,
97 admin_port: resolved_config.admin_port,
98 metrics_port: resolved_config.metrics_port,
99 })
100 }
101
102 pub fn http_port(&self) -> u16 {
104 self.http_port
105 }
106
107 pub fn ws_port(&self) -> Option<u16> {
109 self.ws_port
110 }
111
112 pub fn grpc_port(&self) -> Option<u16> {
114 self.grpc_port
115 }
116
117 pub fn admin_port(&self) -> Option<u16> {
124 self.admin_port
125 }
126
127 pub fn metrics_port(&self) -> Option<u16> {
129 self.metrics_port
130 }
131
132 pub fn base_url(&self) -> String {
134 format!("http://localhost:{}", self.http_port)
135 }
136
137 pub fn ws_url(&self) -> Option<String> {
139 self.ws_port.map(|port| format!("ws://localhost:{}/ws", port))
140 }
141
142 pub fn pid(&self) -> u32 {
144 self.process.lock().pid()
145 }
146
147 pub fn is_running(&self) -> bool {
149 self.process.lock().is_running()
150 }
151
152 pub async fn health_check(&self) -> Result<HealthStatus> {
154 self.health.check().await
155 }
156
157 pub async fn is_ready(&self) -> bool {
159 self.health.is_ready().await
160 }
161
162 pub async fn scenario(&self, scenario_name: &str) -> Result<()> {
179 self.scenario.switch_scenario(scenario_name).await
180 }
181
182 pub async fn load_workspace<P: AsRef<Path>>(&self, workspace_file: P) -> Result<()> {
184 self.scenario.load_workspace(workspace_file).await
185 }
186
187 pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
189 self.scenario.update_mock(endpoint, config).await
190 }
191
192 pub async fn list_fixtures(&self) -> Result<Vec<String>> {
194 self.scenario.list_fixtures().await
195 }
196
197 pub async fn get_stats(&self) -> Result<Value> {
199 self.scenario.get_stats().await
200 }
201
202 pub async fn reset(&self) -> Result<()> {
204 self.scenario.reset().await
205 }
206
207 pub fn stop(&self) -> Result<()> {
209 info!("Stopping MockForge server (port: {})", self.http_port);
210 self.process.lock().kill()
211 }
212}
213
214impl Drop for MockForgeServer {
215 fn drop(&mut self) {
216 if let Err(e) = self.stop() {
217 error!("Failed to stop MockForge server on drop: {}", e);
218 }
219 }
220}
221
222pub struct MockForgeServerBuilder {
224 config_builder: ServerConfigBuilder,
225}
226
227impl Default for MockForgeServerBuilder {
228 fn default() -> Self {
229 Self {
230 config_builder: ServerConfig::builder(),
231 }
232 }
233}
234
235impl MockForgeServerBuilder {
236 pub fn http_port(mut self, port: u16) -> Self {
238 self.config_builder = self.config_builder.http_port(port);
239 self
240 }
241
242 pub fn ws_port(mut self, port: u16) -> Self {
244 self.config_builder = self.config_builder.ws_port(port);
245 self
246 }
247
248 pub fn grpc_port(mut self, port: u16) -> Self {
250 self.config_builder = self.config_builder.grpc_port(port);
251 self
252 }
253
254 pub fn admin_port(mut self, port: u16) -> Self {
256 self.config_builder = self.config_builder.admin_port(port);
257 self
258 }
259
260 pub fn metrics_port(mut self, port: u16) -> Self {
262 self.config_builder = self.config_builder.metrics_port(port);
263 self
264 }
265
266 pub fn spec_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
268 self.config_builder = self.config_builder.spec_file(path);
269 self
270 }
271
272 pub fn workspace_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
274 self.config_builder = self.config_builder.workspace_dir(path);
275 self
276 }
277
278 pub fn profile(mut self, profile: impl Into<String>) -> Self {
280 self.config_builder = self.config_builder.profile(profile);
281 self
282 }
283
284 pub fn enable_admin(mut self, enable: bool) -> Self {
286 self.config_builder = self.config_builder.enable_admin(enable);
287 self
288 }
289
290 pub fn enable_metrics(mut self, enable: bool) -> Self {
292 self.config_builder = self.config_builder.enable_metrics(enable);
293 self
294 }
295
296 pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
298 self.config_builder = self.config_builder.extra_arg(arg);
299 self
300 }
301
302 pub fn health_timeout(mut self, timeout: std::time::Duration) -> Self {
304 self.config_builder = self.config_builder.health_timeout(timeout);
305 self
306 }
307
308 pub fn working_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
310 self.config_builder = self.config_builder.working_dir(path);
311 self
312 }
313
314 pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
316 self.config_builder = self.config_builder.env_var(key, value);
317 self
318 }
319
320 pub fn binary_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
322 self.config_builder = self.config_builder.binary_path(path);
323 self
324 }
325
326 pub async fn build(self) -> Result<MockForgeServer> {
328 let config = self.config_builder.build();
329 MockForgeServer::start(config).await
330 }
331}
332
333pub async fn with_mockforge<F, Fut>(test: F) -> Result<()>
336where
337 F: FnOnce(MockForgeServer) -> Fut,
338 Fut: std::future::Future<Output = Result<()>>,
339{
340 let server = MockForgeServer::builder().build().await?;
341 test(server).await
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use std::time::Duration;
348
349 #[test]
350 fn test_builder_creation() {
351 let builder = MockForgeServer::builder().http_port(3000).enable_admin(true).profile("test");
352 drop(builder);
354 }
355
356 #[test]
357 fn test_builder_default() {
358 let _builder = MockForgeServerBuilder::default();
359 }
361
362 #[test]
363 fn test_builder_http_port() {
364 let builder = MockForgeServer::builder().http_port(8080);
365 let _builder = builder.http_port(9090);
367 }
368
369 #[test]
370 fn test_builder_ws_port() {
371 let _builder = MockForgeServer::builder().ws_port(3001);
372 }
373
374 #[test]
375 fn test_builder_grpc_port() {
376 let _builder = MockForgeServer::builder().grpc_port(50051);
377 }
378
379 #[test]
380 fn test_builder_admin_port() {
381 let _builder = MockForgeServer::builder().admin_port(3002);
382 }
383
384 #[test]
385 fn test_builder_metrics_port() {
386 let _builder = MockForgeServer::builder().metrics_port(9090);
387 }
388
389 #[test]
390 fn test_builder_spec_file() {
391 let _builder = MockForgeServer::builder().spec_file("/path/to/spec.yaml");
392 }
393
394 #[test]
395 fn test_builder_workspace_dir() {
396 let _builder = MockForgeServer::builder().workspace_dir("/path/to/workspace");
397 }
398
399 #[test]
400 fn test_builder_profile() {
401 let _builder = MockForgeServer::builder().profile("production");
402 }
403
404 #[test]
405 fn test_builder_enable_admin() {
406 let _builder = MockForgeServer::builder().enable_admin(true);
407 let _builder2 = MockForgeServer::builder().enable_admin(false);
408 }
409
410 #[test]
411 fn test_builder_enable_metrics() {
412 let _builder = MockForgeServer::builder().enable_metrics(true);
413 let _builder2 = MockForgeServer::builder().enable_metrics(false);
414 }
415
416 #[test]
417 fn test_builder_extra_arg() {
418 let _builder = MockForgeServer::builder().extra_arg("--verbose");
419 }
420
421 #[test]
422 fn test_builder_health_timeout() {
423 let _builder = MockForgeServer::builder().health_timeout(Duration::from_secs(60));
424 }
425
426 #[test]
427 fn test_builder_working_dir() {
428 let _builder = MockForgeServer::builder().working_dir("/tmp/test");
429 }
430
431 #[test]
432 fn test_builder_env_var() {
433 let _builder = MockForgeServer::builder().env_var("RUST_LOG", "debug");
434 }
435
436 #[test]
437 fn test_builder_binary_path() {
438 let _builder = MockForgeServer::builder().binary_path("/usr/local/bin/mockforge");
439 }
440
441 #[test]
442 fn test_builder_full_chain() {
443 let _builder = MockForgeServer::builder()
444 .http_port(3000)
445 .ws_port(3001)
446 .grpc_port(50051)
447 .admin_port(3002)
448 .metrics_port(9090)
449 .spec_file("/spec.yaml")
450 .workspace_dir("/workspace")
451 .profile("test")
452 .enable_admin(true)
453 .enable_metrics(true)
454 .extra_arg("--verbose")
455 .health_timeout(Duration::from_secs(30))
456 .working_dir("/working")
457 .env_var("KEY", "VALUE")
458 .binary_path("/bin/mockforge");
459
460 }
462}