1use crate::config::{ServerConfig, ServerConfigBuilder};
4use crate::error::{Error, Result};
5use crate::health::{HealthCheck, HealthStatus};
6use crate::port_reservation::PortReservation;
7use crate::process::ManagedProcess;
8use crate::scenario::ScenarioManager;
9use parking_lot::Mutex;
10use serde_json::Value;
11use std::path::Path;
12use std::sync::Arc;
13use tracing::{debug, error, info};
14
15pub struct MockForgeServer {
17 process: Arc<Mutex<ManagedProcess>>,
18 health: HealthCheck,
19 scenario: ScenarioManager,
20 http_port: u16,
21 ws_port: Option<u16>,
22 grpc_port: Option<u16>,
23 admin_port: Option<u16>,
24 metrics_port: Option<u16>,
25}
26
27impl MockForgeServer {
28 pub fn builder() -> MockForgeServerBuilder {
30 MockForgeServerBuilder::default()
31 }
32
33 pub async fn start(config: ServerConfig) -> Result<Self> {
35 let mut resolved_config = config.clone();
41 let mut reservations = Vec::new();
42 let mut reserve_port = || -> Result<u16> {
43 let reservation = PortReservation::new()
44 .map_err(|e| Error::ConfigError(format!("Port reservation failed: {e}")))?;
45 let port = reservation.port();
46 reservations.push(reservation);
47 Ok(port)
48 };
49 if resolved_config.http_port == 0 {
50 resolved_config.http_port = reserve_port()?;
51 info!("Auto-assigned HTTP port: {}", resolved_config.http_port);
52 }
53 if resolved_config.admin_port == Some(0) {
54 let port = reserve_port()?;
55 resolved_config.admin_port = Some(port);
56 info!("Auto-assigned admin port: {}", port);
57 }
58 if resolved_config.metrics_port.is_none() || resolved_config.metrics_port == Some(0) {
65 let port = reserve_port()?;
66 resolved_config.metrics_port = Some(port);
67 info!("Auto-assigned metrics port: {}", port);
68 }
69 if resolved_config.ws_port.is_none() || resolved_config.ws_port == Some(0) {
75 let port = reserve_port()?;
76 resolved_config.ws_port = Some(port);
77 info!("Auto-assigned WebSocket port: {}", port);
78 }
79 if resolved_config.grpc_port.is_none() || resolved_config.grpc_port == Some(0) {
80 let port = reserve_port()?;
81 resolved_config.grpc_port = Some(port);
82 info!("Auto-assigned gRPC port: {}", port);
83 }
84
85 let process = ManagedProcess::spawn_reserved(&resolved_config, reservations)?;
87 let http_port = process.http_port();
88
89 info!("MockForge server started on port {}", http_port);
90
91 let health = HealthCheck::new("localhost", http_port);
93
94 debug!("Waiting for server to become healthy...");
96 health
97 .wait_until_healthy(resolved_config.health_timeout, resolved_config.health_interval)
98 .await?;
99
100 info!("MockForge server is healthy and ready");
101
102 let scenario = ScenarioManager::new("localhost", http_port);
104
105 Ok(Self {
106 process: Arc::new(Mutex::new(process)),
107 health,
108 scenario,
109 http_port,
110 ws_port: resolved_config.ws_port,
111 grpc_port: resolved_config.grpc_port,
112 admin_port: resolved_config.admin_port,
113 metrics_port: resolved_config.metrics_port,
114 })
115 }
116
117 pub fn http_port(&self) -> u16 {
119 self.http_port
120 }
121
122 pub fn ws_port(&self) -> Option<u16> {
124 self.ws_port
125 }
126
127 pub fn grpc_port(&self) -> Option<u16> {
129 self.grpc_port
130 }
131
132 pub fn admin_port(&self) -> Option<u16> {
139 self.admin_port
140 }
141
142 pub fn metrics_port(&self) -> Option<u16> {
144 self.metrics_port
145 }
146
147 pub fn base_url(&self) -> String {
149 format!("http://localhost:{}", self.http_port)
150 }
151
152 pub fn ws_url(&self) -> Option<String> {
154 self.ws_port.map(|port| format!("ws://localhost:{}/ws", port))
155 }
156
157 pub fn pid(&self) -> u32 {
159 self.process.lock().pid()
160 }
161
162 pub fn is_running(&self) -> bool {
164 self.process.lock().is_running()
165 }
166
167 pub async fn health_check(&self) -> Result<HealthStatus> {
169 self.health.check().await
170 }
171
172 pub async fn is_ready(&self) -> bool {
174 self.health.is_ready().await
175 }
176
177 pub async fn scenario(&self, scenario_name: &str) -> Result<()> {
194 self.scenario.switch_scenario(scenario_name).await
195 }
196
197 pub async fn load_workspace<P: AsRef<Path>>(&self, workspace_file: P) -> Result<()> {
199 self.scenario.load_workspace(workspace_file).await
200 }
201
202 pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
204 self.scenario.update_mock(endpoint, config).await
205 }
206
207 pub async fn list_fixtures(&self) -> Result<Vec<String>> {
209 self.scenario.list_fixtures().await
210 }
211
212 pub async fn get_stats(&self) -> Result<Value> {
214 self.scenario.get_stats().await
215 }
216
217 pub async fn reset(&self) -> Result<()> {
219 self.scenario.reset().await
220 }
221
222 pub fn stop(&self) -> Result<()> {
224 info!("Stopping MockForge server (port: {})", self.http_port);
225 self.process.lock().kill()
226 }
227}
228
229impl Drop for MockForgeServer {
230 fn drop(&mut self) {
231 if let Err(e) = self.stop() {
232 error!("Failed to stop MockForge server on drop: {}", e);
233 }
234 }
235}
236
237pub struct MockForgeServerBuilder {
239 config_builder: ServerConfigBuilder,
240}
241
242impl Default for MockForgeServerBuilder {
243 fn default() -> Self {
244 Self {
245 config_builder: ServerConfig::builder(),
246 }
247 }
248}
249
250impl MockForgeServerBuilder {
251 pub fn http_port(mut self, port: u16) -> Self {
253 self.config_builder = self.config_builder.http_port(port);
254 self
255 }
256
257 pub fn ws_port(mut self, port: u16) -> Self {
259 self.config_builder = self.config_builder.ws_port(port);
260 self
261 }
262
263 pub fn grpc_port(mut self, port: u16) -> Self {
265 self.config_builder = self.config_builder.grpc_port(port);
266 self
267 }
268
269 pub fn admin_port(mut self, port: u16) -> Self {
271 self.config_builder = self.config_builder.admin_port(port);
272 self
273 }
274
275 pub fn metrics_port(mut self, port: u16) -> Self {
277 self.config_builder = self.config_builder.metrics_port(port);
278 self
279 }
280
281 pub fn spec_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
283 self.config_builder = self.config_builder.spec_file(path);
284 self
285 }
286
287 pub fn workspace_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
289 self.config_builder = self.config_builder.workspace_dir(path);
290 self
291 }
292
293 pub fn profile(mut self, profile: impl Into<String>) -> Self {
295 self.config_builder = self.config_builder.profile(profile);
296 self
297 }
298
299 pub fn enable_admin(mut self, enable: bool) -> Self {
301 self.config_builder = self.config_builder.enable_admin(enable);
302 self
303 }
304
305 pub fn enable_metrics(mut self, enable: bool) -> Self {
307 self.config_builder = self.config_builder.enable_metrics(enable);
308 self
309 }
310
311 pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
313 self.config_builder = self.config_builder.extra_arg(arg);
314 self
315 }
316
317 pub fn health_timeout(mut self, timeout: std::time::Duration) -> Self {
319 self.config_builder = self.config_builder.health_timeout(timeout);
320 self
321 }
322
323 pub fn working_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
325 self.config_builder = self.config_builder.working_dir(path);
326 self
327 }
328
329 pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
331 self.config_builder = self.config_builder.env_var(key, value);
332 self
333 }
334
335 pub fn binary_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
337 self.config_builder = self.config_builder.binary_path(path);
338 self
339 }
340
341 pub async fn build(self) -> Result<MockForgeServer> {
343 let config = self.config_builder.build();
344 MockForgeServer::start(config).await
345 }
346}
347
348pub async fn with_mockforge<F, Fut>(test: F) -> Result<()>
351where
352 F: FnOnce(MockForgeServer) -> Fut,
353 Fut: std::future::Future<Output = Result<()>>,
354{
355 let server = MockForgeServer::builder().build().await?;
356 test(server).await
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use std::time::Duration;
363
364 #[test]
365 fn test_builder_creation() {
366 let builder = MockForgeServer::builder().http_port(3000).enable_admin(true).profile("test");
367 drop(builder);
369 }
370
371 #[test]
372 fn test_builder_default() {
373 let _builder = MockForgeServerBuilder::default();
374 }
376
377 #[test]
378 fn test_builder_http_port() {
379 let builder = MockForgeServer::builder().http_port(8080);
380 let _builder = builder.http_port(9090);
382 }
383
384 #[test]
385 fn test_builder_ws_port() {
386 let _builder = MockForgeServer::builder().ws_port(3001);
387 }
388
389 #[test]
390 fn test_builder_grpc_port() {
391 let _builder = MockForgeServer::builder().grpc_port(50051);
392 }
393
394 #[test]
395 fn test_builder_admin_port() {
396 let _builder = MockForgeServer::builder().admin_port(3002);
397 }
398
399 #[test]
400 fn test_builder_metrics_port() {
401 let _builder = MockForgeServer::builder().metrics_port(9090);
402 }
403
404 #[test]
405 fn test_builder_spec_file() {
406 let _builder = MockForgeServer::builder().spec_file("/path/to/spec.yaml");
407 }
408
409 #[test]
410 fn test_builder_workspace_dir() {
411 let _builder = MockForgeServer::builder().workspace_dir("/path/to/workspace");
412 }
413
414 #[test]
415 fn test_builder_profile() {
416 let _builder = MockForgeServer::builder().profile("production");
417 }
418
419 #[test]
420 fn test_builder_enable_admin() {
421 let _builder = MockForgeServer::builder().enable_admin(true);
422 let _builder2 = MockForgeServer::builder().enable_admin(false);
423 }
424
425 #[test]
426 fn test_builder_enable_metrics() {
427 let _builder = MockForgeServer::builder().enable_metrics(true);
428 let _builder2 = MockForgeServer::builder().enable_metrics(false);
429 }
430
431 #[test]
432 fn test_builder_extra_arg() {
433 let _builder = MockForgeServer::builder().extra_arg("--verbose");
434 }
435
436 #[test]
437 fn test_builder_health_timeout() {
438 let _builder = MockForgeServer::builder().health_timeout(Duration::from_secs(60));
439 }
440
441 #[test]
442 fn test_builder_working_dir() {
443 let _builder = MockForgeServer::builder().working_dir("/tmp/test");
444 }
445
446 #[test]
447 fn test_builder_env_var() {
448 let _builder = MockForgeServer::builder().env_var("RUST_LOG", "debug");
449 }
450
451 #[test]
452 fn test_builder_binary_path() {
453 let _builder = MockForgeServer::builder().binary_path("/usr/local/bin/mockforge");
454 }
455
456 #[test]
457 fn test_builder_full_chain() {
458 let _builder = MockForgeServer::builder()
459 .http_port(3000)
460 .ws_port(3001)
461 .grpc_port(50051)
462 .admin_port(3002)
463 .metrics_port(9090)
464 .spec_file("/spec.yaml")
465 .workspace_dir("/workspace")
466 .profile("test")
467 .enable_admin(true)
468 .enable_metrics(true)
469 .extra_arg("--verbose")
470 .health_timeout(Duration::from_secs(30))
471 .working_dir("/working")
472 .env_var("KEY", "VALUE")
473 .binary_path("/bin/mockforge");
474
475 }
477}