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