use crate::config::{ServerConfig, ServerConfigBuilder};
use crate::error::Result;
use crate::health::{HealthCheck, HealthStatus};
use crate::process::{find_available_port, ManagedProcess};
use crate::scenario::ScenarioManager;
use parking_lot::Mutex;
use serde_json::Value;
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, error, info};
pub struct MockForgeServer {
process: Arc<Mutex<ManagedProcess>>,
health: HealthCheck,
scenario: ScenarioManager,
http_port: u16,
ws_port: Option<u16>,
grpc_port: Option<u16>,
admin_port: Option<u16>,
metrics_port: Option<u16>,
}
impl MockForgeServer {
pub fn builder() -> MockForgeServerBuilder {
MockForgeServerBuilder::default()
}
pub async fn start(config: ServerConfig) -> Result<Self> {
let mut resolved_config = config.clone();
if resolved_config.http_port == 0 {
resolved_config.http_port = find_available_port(30000)?;
info!("Auto-assigned HTTP port: {}", resolved_config.http_port);
}
if resolved_config.admin_port == Some(0) {
let port = find_available_port(31000)?;
resolved_config.admin_port = Some(port);
info!("Auto-assigned admin port: {}", port);
}
if resolved_config.metrics_port == Some(0) {
let port = find_available_port(32000)?;
resolved_config.metrics_port = Some(port);
info!("Auto-assigned metrics port: {}", port);
}
if resolved_config.ws_port.is_none() || resolved_config.ws_port == Some(0) {
let port = find_available_port(33000)?;
resolved_config.ws_port = Some(port);
info!("Auto-assigned WebSocket port: {}", port);
}
if resolved_config.grpc_port.is_none() || resolved_config.grpc_port == Some(0) {
let port = find_available_port(34000)?;
resolved_config.grpc_port = Some(port);
info!("Auto-assigned gRPC port: {}", port);
}
let process = ManagedProcess::spawn(&resolved_config)?;
let http_port = process.http_port();
info!("MockForge server started on port {}", http_port);
let health = HealthCheck::new("localhost", http_port);
debug!("Waiting for server to become healthy...");
health
.wait_until_healthy(resolved_config.health_timeout, resolved_config.health_interval)
.await?;
info!("MockForge server is healthy and ready");
let scenario = ScenarioManager::new("localhost", http_port);
Ok(Self {
process: Arc::new(Mutex::new(process)),
health,
scenario,
http_port,
ws_port: resolved_config.ws_port,
grpc_port: resolved_config.grpc_port,
admin_port: resolved_config.admin_port,
metrics_port: resolved_config.metrics_port,
})
}
pub fn http_port(&self) -> u16 {
self.http_port
}
pub fn ws_port(&self) -> Option<u16> {
self.ws_port
}
pub fn grpc_port(&self) -> Option<u16> {
self.grpc_port
}
pub fn admin_port(&self) -> Option<u16> {
self.admin_port
}
pub fn metrics_port(&self) -> Option<u16> {
self.metrics_port
}
pub fn base_url(&self) -> String {
format!("http://localhost:{}", self.http_port)
}
pub fn ws_url(&self) -> Option<String> {
self.ws_port.map(|port| format!("ws://localhost:{}/ws", port))
}
pub fn pid(&self) -> u32 {
self.process.lock().pid()
}
pub fn is_running(&self) -> bool {
self.process.lock().is_running()
}
pub async fn health_check(&self) -> Result<HealthStatus> {
self.health.check().await
}
pub async fn is_ready(&self) -> bool {
self.health.is_ready().await
}
pub async fn scenario(&self, scenario_name: &str) -> Result<()> {
self.scenario.switch_scenario(scenario_name).await
}
pub async fn load_workspace<P: AsRef<Path>>(&self, workspace_file: P) -> Result<()> {
self.scenario.load_workspace(workspace_file).await
}
pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
self.scenario.update_mock(endpoint, config).await
}
pub async fn list_fixtures(&self) -> Result<Vec<String>> {
self.scenario.list_fixtures().await
}
pub async fn get_stats(&self) -> Result<Value> {
self.scenario.get_stats().await
}
pub async fn reset(&self) -> Result<()> {
self.scenario.reset().await
}
pub fn stop(&self) -> Result<()> {
info!("Stopping MockForge server (port: {})", self.http_port);
self.process.lock().kill()
}
}
impl Drop for MockForgeServer {
fn drop(&mut self) {
if let Err(e) = self.stop() {
error!("Failed to stop MockForge server on drop: {}", e);
}
}
}
pub struct MockForgeServerBuilder {
config_builder: ServerConfigBuilder,
}
impl Default for MockForgeServerBuilder {
fn default() -> Self {
Self {
config_builder: ServerConfig::builder(),
}
}
}
impl MockForgeServerBuilder {
pub fn http_port(mut self, port: u16) -> Self {
self.config_builder = self.config_builder.http_port(port);
self
}
pub fn ws_port(mut self, port: u16) -> Self {
self.config_builder = self.config_builder.ws_port(port);
self
}
pub fn grpc_port(mut self, port: u16) -> Self {
self.config_builder = self.config_builder.grpc_port(port);
self
}
pub fn admin_port(mut self, port: u16) -> Self {
self.config_builder = self.config_builder.admin_port(port);
self
}
pub fn metrics_port(mut self, port: u16) -> Self {
self.config_builder = self.config_builder.metrics_port(port);
self
}
pub fn spec_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.config_builder = self.config_builder.spec_file(path);
self
}
pub fn workspace_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.config_builder = self.config_builder.workspace_dir(path);
self
}
pub fn profile(mut self, profile: impl Into<String>) -> Self {
self.config_builder = self.config_builder.profile(profile);
self
}
pub fn enable_admin(mut self, enable: bool) -> Self {
self.config_builder = self.config_builder.enable_admin(enable);
self
}
pub fn enable_metrics(mut self, enable: bool) -> Self {
self.config_builder = self.config_builder.enable_metrics(enable);
self
}
pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
self.config_builder = self.config_builder.extra_arg(arg);
self
}
pub fn health_timeout(mut self, timeout: std::time::Duration) -> Self {
self.config_builder = self.config_builder.health_timeout(timeout);
self
}
pub fn working_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.config_builder = self.config_builder.working_dir(path);
self
}
pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config_builder = self.config_builder.env_var(key, value);
self
}
pub fn binary_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.config_builder = self.config_builder.binary_path(path);
self
}
pub async fn build(self) -> Result<MockForgeServer> {
let config = self.config_builder.build();
MockForgeServer::start(config).await
}
}
pub async fn with_mockforge<F, Fut>(test: F) -> Result<()>
where
F: FnOnce(MockForgeServer) -> Fut,
Fut: std::future::Future<Output = Result<()>>,
{
let server = MockForgeServer::builder().build().await?;
test(server).await
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_builder_creation() {
let builder = MockForgeServer::builder().http_port(3000).enable_admin(true).profile("test");
drop(builder);
}
#[test]
fn test_builder_default() {
let _builder = MockForgeServerBuilder::default();
}
#[test]
fn test_builder_http_port() {
let builder = MockForgeServer::builder().http_port(8080);
let _builder = builder.http_port(9090);
}
#[test]
fn test_builder_ws_port() {
let _builder = MockForgeServer::builder().ws_port(3001);
}
#[test]
fn test_builder_grpc_port() {
let _builder = MockForgeServer::builder().grpc_port(50051);
}
#[test]
fn test_builder_admin_port() {
let _builder = MockForgeServer::builder().admin_port(3002);
}
#[test]
fn test_builder_metrics_port() {
let _builder = MockForgeServer::builder().metrics_port(9090);
}
#[test]
fn test_builder_spec_file() {
let _builder = MockForgeServer::builder().spec_file("/path/to/spec.yaml");
}
#[test]
fn test_builder_workspace_dir() {
let _builder = MockForgeServer::builder().workspace_dir("/path/to/workspace");
}
#[test]
fn test_builder_profile() {
let _builder = MockForgeServer::builder().profile("production");
}
#[test]
fn test_builder_enable_admin() {
let _builder = MockForgeServer::builder().enable_admin(true);
let _builder2 = MockForgeServer::builder().enable_admin(false);
}
#[test]
fn test_builder_enable_metrics() {
let _builder = MockForgeServer::builder().enable_metrics(true);
let _builder2 = MockForgeServer::builder().enable_metrics(false);
}
#[test]
fn test_builder_extra_arg() {
let _builder = MockForgeServer::builder().extra_arg("--verbose");
}
#[test]
fn test_builder_health_timeout() {
let _builder = MockForgeServer::builder().health_timeout(Duration::from_secs(60));
}
#[test]
fn test_builder_working_dir() {
let _builder = MockForgeServer::builder().working_dir("/tmp/test");
}
#[test]
fn test_builder_env_var() {
let _builder = MockForgeServer::builder().env_var("RUST_LOG", "debug");
}
#[test]
fn test_builder_binary_path() {
let _builder = MockForgeServer::builder().binary_path("/usr/local/bin/mockforge");
}
#[test]
fn test_builder_full_chain() {
let _builder = MockForgeServer::builder()
.http_port(3000)
.ws_port(3001)
.grpc_port(50051)
.admin_port(3002)
.metrics_port(9090)
.spec_file("/spec.yaml")
.workspace_dir("/workspace")
.profile("test")
.enable_admin(true)
.enable_metrics(true)
.extra_arg("--verbose")
.health_timeout(Duration::from_secs(30))
.working_dir("/working")
.env_var("KEY", "VALUE")
.binary_path("/bin/mockforge");
}
}