pub mod ai_handler;
pub mod auth;
pub mod chain_handlers;
pub mod consistency;
pub mod contract_diff_middleware;
pub mod coverage;
pub mod database;
pub mod file_generator;
pub mod file_server;
pub mod health;
pub mod http_tracing_middleware;
pub mod latency_profiles;
pub mod management;
pub mod management_ws;
pub mod metrics_middleware;
pub mod middleware;
pub mod op_middleware;
pub mod protocol_server;
pub mod proxy_server;
pub mod quick_mock;
pub mod rag_ai_generator;
pub mod replay_listing;
pub mod request_logging;
pub mod spec_import;
pub mod sse;
pub mod state_machine_api;
pub mod tls;
pub mod token_response;
pub mod ui_builder;
pub mod verification;
pub mod handlers;
pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
pub use health::{HealthManager, ServiceStatus};
pub use management::{
management_router, management_router_with_ui_builder, ManagementState, MockConfig,
ServerConfig, ServerStats,
};
pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
pub use verification::verification_router;
pub use metrics_middleware::collect_http_metrics;
pub use http_tracing_middleware::http_tracing_middleware;
pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
async fn load_persona_from_config() -> Option<Arc<Persona>> {
use mockforge_core::config::load_config;
let config_paths = [
"config.yaml",
"mockforge.yaml",
"tools/mockforge/config.yaml",
"../tools/mockforge/config.yaml",
];
for path in &config_paths {
if let Ok(config) = load_config(path).await {
if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona()
{
tracing::info!(
"Loaded active persona '{}' from config file: {}",
persona.name,
path
);
return Some(Arc::new(persona.clone()));
} else {
tracing::debug!(
"No active persona found in config file: {} (personas count: {})",
path,
config.mockai.intelligent_behavior.personas.personas.len()
);
}
} else {
tracing::debug!("Could not load config from: {}", path);
}
}
tracing::debug!("No persona found in config files, persona-based generation will be disabled");
None
}
use axum::extract::State;
use axum::middleware::from_fn_with_state;
use axum::response::Json;
use axum::Router;
use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
use mockforge_core::intelligent_behavior::config::Persona;
use mockforge_core::latency::LatencyInjector;
use mockforge_core::openapi::OpenApiSpec;
use mockforge_core::openapi_routes::OpenApiRouteRegistry;
use mockforge_core::openapi_routes::ValidationOptions;
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use mockforge_core::LatencyProfile;
#[cfg(feature = "data-faker")]
use mockforge_data::provider::register_core_faker_provider;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::Path;
use tokio::fs;
use tokio::sync::RwLock;
use tracing::*;
#[derive(Clone)]
pub struct RouteInfo {
pub method: String,
pub path: String,
pub operation_id: Option<String>,
pub summary: Option<String>,
pub description: Option<String>,
pub parameters: Vec<String>,
}
#[derive(Clone)]
pub struct HttpServerState {
pub routes: Vec<RouteInfo>,
pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
pub production_headers: Option<Arc<HashMap<String, String>>>,
}
impl Default for HttpServerState {
fn default() -> Self {
Self::new()
}
}
impl HttpServerState {
pub fn new() -> Self {
Self {
routes: Vec::new(),
rate_limiter: None,
production_headers: None,
}
}
pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
Self {
routes,
rate_limiter: None,
production_headers: None,
}
}
pub fn with_rate_limiter(
mut self,
rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
) -> Self {
self.rate_limiter = Some(rate_limiter);
self
}
pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
self.production_headers = Some(headers);
self
}
}
async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
let route_info: Vec<serde_json::Value> = state
.routes
.iter()
.map(|route| {
serde_json::json!({
"method": route.method,
"path": route.path,
"operation_id": route.operation_id,
"summary": route.summary,
"description": route.description,
"parameters": route.parameters
})
})
.collect();
Json(serde_json::json!({
"routes": route_info,
"total": state.routes.len()
}))
}
async fn get_docs_handler() -> axum::response::Html<&'static str> {
axum::response::Html(include_str!("../static/docs.html"))
}
pub async fn build_router(
spec_path: Option<String>,
options: Option<ValidationOptions>,
failure_config: Option<FailureConfig>,
) -> Router {
build_router_with_multi_tenant(
spec_path,
options,
failure_config,
None,
None,
None,
None,
None,
None,
None,
)
.await
}
fn apply_cors_middleware(
app: Router,
cors_config: Option<mockforge_core::config::HttpCorsConfig>,
) -> Router {
use http::Method;
use tower_http::cors::AllowOrigin;
if let Some(config) = cors_config {
if !config.enabled {
return app;
}
let mut cors_layer = CorsLayer::new();
let is_wildcard_origin;
if config.allowed_origins.contains(&"*".to_string()) {
cors_layer = cors_layer.allow_origin(Any);
is_wildcard_origin = true;
} else if !config.allowed_origins.is_empty() {
let origins: Vec<_> = config
.allowed_origins
.iter()
.filter_map(|origin| {
origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
})
.collect();
if origins.is_empty() {
warn!("No valid CORS origins configured, using permissive CORS");
cors_layer = cors_layer.allow_origin(Any);
is_wildcard_origin = true;
} else {
if origins.len() == 1 {
cors_layer = cors_layer.allow_origin(origins[0].clone());
is_wildcard_origin = false;
} else {
warn!(
"Multiple CORS origins configured, using permissive CORS. \
Consider using '*' for all origins."
);
cors_layer = cors_layer.allow_origin(Any);
is_wildcard_origin = true;
}
}
} else {
cors_layer = cors_layer.allow_origin(Any);
is_wildcard_origin = true;
}
if !config.allowed_methods.is_empty() {
let methods: Vec<Method> =
config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
if !methods.is_empty() {
cors_layer = cors_layer.allow_methods(methods);
}
} else {
cors_layer = cors_layer.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::PATCH,
Method::OPTIONS,
]);
}
if !config.allowed_headers.is_empty() {
let headers: Vec<_> = config
.allowed_headers
.iter()
.filter_map(|h| h.parse::<http::HeaderName>().ok())
.collect();
if !headers.is_empty() {
cors_layer = cors_layer.allow_headers(headers);
}
} else {
cors_layer =
cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
}
let should_allow_credentials = if is_wildcard_origin {
false
} else {
config.allow_credentials
};
cors_layer = cors_layer.allow_credentials(should_allow_credentials);
info!(
"CORS middleware enabled with configured settings (credentials: {})",
should_allow_credentials
);
app.layer(cors_layer)
} else {
debug!("No CORS config provided, using permissive CORS for development");
app.layer(CorsLayer::permissive().allow_credentials(false))
}
}
#[allow(clippy::too_many_arguments)]
pub async fn build_router_with_multi_tenant(
spec_path: Option<String>,
options: Option<ValidationOptions>,
failure_config: Option<FailureConfig>,
multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
_route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
cors_config: Option<mockforge_core::config::HttpCorsConfig>,
ai_generator: Option<Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>>,
smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
) -> Router {
use std::time::Instant;
let startup_start = Instant::now();
let mut app = Router::new();
let mut rate_limit_config = middleware::RateLimitConfig {
requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1000),
burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2000),
per_ip: true,
per_endpoint: false,
};
let mut final_cors_config = cors_config;
let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
if let Some(deploy_config) = &deceptive_deploy_config {
if deploy_config.enabled {
info!("Deceptive deploy mode enabled - applying production-like configuration");
if let Some(prod_cors) = &deploy_config.cors {
final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
enabled: true,
allowed_origins: prod_cors.allowed_origins.clone(),
allowed_methods: prod_cors.allowed_methods.clone(),
allowed_headers: prod_cors.allowed_headers.clone(),
allow_credentials: prod_cors.allow_credentials,
});
info!("Applied production-like CORS configuration");
}
if let Some(prod_rate_limit) = &deploy_config.rate_limit {
rate_limit_config = middleware::RateLimitConfig {
requests_per_minute: prod_rate_limit.requests_per_minute,
burst: prod_rate_limit.burst,
per_ip: prod_rate_limit.per_ip,
per_endpoint: false,
};
info!(
"Applied production-like rate limiting: {} req/min, burst: {}",
prod_rate_limit.requests_per_minute, prod_rate_limit.burst
);
}
if !deploy_config.headers.is_empty() {
let headers_map: HashMap<String, String> = deploy_config.headers.clone();
production_headers = Some(std::sync::Arc::new(headers_map));
info!("Configured {} production headers", deploy_config.headers.len());
}
if let Some(prod_oauth) = &deploy_config.oauth {
let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
oauth2: Some(oauth2_config),
..Default::default()
});
info!("Applied production-like OAuth configuration for deceptive deploy");
}
}
}
let rate_limiter =
std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
if let Some(headers) = production_headers.clone() {
state = state.with_production_headers(headers);
}
let spec_path_for_mgmt = spec_path.clone();
if let Some(spec_path) = spec_path {
tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
let spec_load_start = Instant::now();
match OpenApiSpec::from_file(&spec_path).await {
Ok(openapi) => {
let spec_load_duration = spec_load_start.elapsed();
info!(
"Successfully loaded OpenAPI spec from {} (took {:?})",
spec_path, spec_load_duration
);
tracing::debug!("Creating OpenAPI route registry...");
let registry_start = Instant::now();
let persona = load_persona_from_config().await;
let registry = if let Some(opts) = options {
tracing::debug!("Using custom validation options");
if let Some(ref persona) = persona {
tracing::info!("Using persona '{}' for route generation", persona.name);
}
OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
} else {
tracing::debug!("Using environment-based options");
if let Some(ref persona) = persona {
tracing::info!("Using persona '{}' for route generation", persona.name);
}
OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
};
let registry_duration = registry_start.elapsed();
info!(
"Created OpenAPI route registry with {} routes (took {:?})",
registry.routes().len(),
registry_duration
);
let extract_start = Instant::now();
let route_info: Vec<RouteInfo> = registry
.routes()
.iter()
.map(|route| RouteInfo {
method: route.method.clone(),
path: route.path.clone(),
operation_id: route.operation.operation_id.clone(),
summary: route.operation.summary.clone(),
description: route.operation.description.clone(),
parameters: route.parameters.clone(),
})
.collect();
state.routes = route_info;
let extract_duration = extract_start.elapsed();
debug!("Extracted route information (took {:?})", extract_duration);
let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
tracing::debug!("Loading overrides from environment variable");
let overrides_start = Instant::now();
match mockforge_core::Overrides::load_from_globs(&[]).await {
Ok(overrides) => {
let overrides_duration = overrides_start.elapsed();
info!(
"Loaded {} override rules (took {:?})",
overrides.rules().len(),
overrides_duration
);
Some(overrides)
}
Err(e) => {
tracing::warn!("Failed to load overrides: {}", e);
None
}
}
} else {
None
};
let router_build_start = Instant::now();
let overrides_enabled = overrides.is_some();
let openapi_router = if let Some(mockai_instance) = &mockai {
tracing::debug!("Building router with MockAI support");
registry.build_router_with_mockai(Some(mockai_instance.clone()))
} else if let Some(ai_generator) = &ai_generator {
tracing::debug!("Building router with AI generator support");
registry.build_router_with_ai(Some(ai_generator.clone()))
} else if let Some(failure_config) = &failure_config {
tracing::debug!("Building router with failure injection and overrides");
let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
registry.build_router_with_injectors_and_overrides(
LatencyInjector::default(),
Some(failure_injector),
overrides,
overrides_enabled,
)
} else {
tracing::debug!("Building router with overrides");
registry.build_router_with_injectors_and_overrides(
LatencyInjector::default(),
None,
overrides,
overrides_enabled,
)
};
let router_build_duration = router_build_start.elapsed();
debug!("Built OpenAPI router (took {:?})", router_build_duration);
tracing::debug!("Merging OpenAPI router with main router");
app = app.merge(openapi_router);
tracing::debug!("Router built successfully");
}
Err(e) => {
warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
}
}
}
app = app.route(
"/health",
axum::routing::get(|| async {
use mockforge_core::server_utils::health::HealthStatus;
{
match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
Ok(value) => Json(value),
Err(e) => {
tracing::error!("Failed to serialize health status: {}", e);
Json(serde_json::json!({
"status": "healthy",
"service": "mockforge-http",
"uptime_seconds": 0
}))
}
}
}
}),
)
.merge(sse::sse_router())
.merge(file_server::file_serving_router());
let state_for_routes = state.clone();
let routes_router = Router::new()
.route("/__mockforge/routes", axum::routing::get(get_routes_handler))
.route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
.with_state(state_for_routes);
app = app.merge(routes_router);
app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
.unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
if Path::new(&coverage_html_path).exists() {
app = app.nest_service(
"/__mockforge/coverage.html",
tower_http::services::ServeFile::new(&coverage_html_path),
);
debug!("Serving coverage UI from: {}", coverage_html_path);
} else {
debug!(
"Coverage UI file not found at: {}. Skipping static file serving.",
coverage_html_path
);
}
let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
match OpenApiSpec::from_file(sp).await {
Ok(s) => Some(Arc::new(s)),
Err(e) => {
debug!("Failed to load OpenAPI spec for management API: {}", e);
None
}
}
} else {
None
};
let mgmt_port = std::env::var("PORT")
.or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(3000);
let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
use std::sync::Arc;
let ws_state = WsManagementState::new();
let ws_broadcast = Arc::new(ws_state.tx.clone());
let management_state = management_state.with_ws_broadcast(ws_broadcast);
#[cfg(feature = "smtp")]
let management_state = {
if let Some(smtp_reg) = smtp_registry {
match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
Err(e) => {
error!(
"Invalid SMTP registry type passed to HTTP management state: {:?}",
e.type_id()
);
management_state
}
}
} else {
management_state
}
};
#[cfg(not(feature = "smtp"))]
let management_state = management_state;
#[cfg(not(feature = "smtp"))]
let _ = smtp_registry;
app = app.nest("/__mockforge/api", management_router(management_state));
app = app.merge(verification_router());
use crate::auth::oidc::oidc_router;
app = app.merge(oidc_router());
{
use mockforge_core::security::get_global_access_review_service;
if let Some(service) = get_global_access_review_service().await {
use crate::handlers::access_review::{access_review_router, AccessReviewState};
let review_state = AccessReviewState { service };
app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
debug!("Access review API mounted at /api/v1/security/access-reviews");
}
}
{
use mockforge_core::security::get_global_privileged_access_manager;
if let Some(manager) = get_global_privileged_access_manager().await {
use crate::handlers::privileged_access::{
privileged_access_router, PrivilegedAccessState,
};
let privileged_state = PrivilegedAccessState { manager };
app = app.nest(
"/api/v1/security/privileged-access",
privileged_access_router(privileged_state),
);
debug!("Privileged access API mounted at /api/v1/security/privileged-access");
}
}
{
use mockforge_core::security::get_global_change_management_engine;
if let Some(engine) = get_global_change_management_engine().await {
use crate::handlers::change_management::{
change_management_router, ChangeManagementState,
};
let change_state = ChangeManagementState { engine };
app = app.nest("/api/v1/change-management", change_management_router(change_state));
debug!("Change management API mounted at /api/v1/change-management");
}
}
{
use mockforge_core::security::get_global_risk_assessment_engine;
if let Some(engine) = get_global_risk_assessment_engine().await {
use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
let risk_state = RiskAssessmentState { engine };
app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
debug!("Risk assessment API mounted at /api/v1/security/risks");
}
}
{
use crate::auth::token_lifecycle::TokenLifecycleManager;
use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
let lifecycle_state = TokenLifecycleState {
manager: lifecycle_manager,
};
app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
debug!("Token lifecycle API mounted at /api/v1/auth");
}
{
use crate::auth::oidc::load_oidc_state;
use crate::auth::token_lifecycle::TokenLifecycleManager;
use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
let oauth2_state = OAuth2ServerState {
oidc_state,
lifecycle_manager,
auth_codes: Arc::new(RwLock::new(HashMap::new())),
refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
};
app = app.merge(oauth2_server_router(oauth2_state));
debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
}
{
use crate::auth::oidc::load_oidc_state;
use crate::auth::risk_engine::RiskEngine;
use crate::auth::token_lifecycle::TokenLifecycleManager;
use crate::handlers::consent::{consent_router, ConsentState};
use crate::handlers::oauth2_server::OAuth2ServerState;
let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
let oauth2_state = OAuth2ServerState {
oidc_state: oidc_state.clone(),
lifecycle_manager: lifecycle_manager.clone(),
auth_codes: Arc::new(RwLock::new(HashMap::new())),
refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
};
let risk_engine = Arc::new(RiskEngine::default());
let consent_state = ConsentState {
oauth2_state,
risk_engine,
};
app = app.merge(consent_router(consent_state));
debug!("Consent screen endpoints mounted at /consent");
}
{
use crate::auth::risk_engine::RiskEngine;
use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
let risk_engine = Arc::new(RiskEngine::default());
let risk_state = RiskSimulationState { risk_engine };
app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
debug!("Risk simulation API mounted at /api/v1/auth/risk");
}
app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
if state.production_headers.is_some() {
app =
app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
}
if let Some(auth_config) = deceptive_deploy_auth_config {
use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
match create_oauth2_client(oauth2_config) {
Ok(client) => Some(client),
Err(e) => {
warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
None
}
}
} else {
None
};
let auth_state = AuthState {
config: auth_config,
spec: None, oauth2_client,
introspection_cache: Arc::new(RwLock::new(HashMap::new())),
};
app = app.layer(from_fn_with_state(auth_state, auth_middleware));
info!("Applied OAuth authentication middleware from deceptive deploy configuration");
}
app = apply_cors_middleware(app, final_cors_config);
if let Some(mt_config) = multi_tenant_config {
if mt_config.enabled {
use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
use std::sync::Arc;
info!(
"Multi-tenant mode enabled with {} routing strategy",
match mt_config.routing_strategy {
mockforge_core::RoutingStrategy::Path => "path-based",
mockforge_core::RoutingStrategy::Port => "port-based",
mockforge_core::RoutingStrategy::Both => "hybrid",
}
);
let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
let default_workspace =
mockforge_core::Workspace::new(mt_config.default_workspace.clone());
if let Err(e) =
registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
{
warn!("Failed to register default workspace: {}", e);
} else {
info!("Registered default workspace: '{}'", mt_config.default_workspace);
}
if mt_config.auto_discover {
if let Some(config_dir) = &mt_config.config_directory {
let config_path = Path::new(config_dir);
if config_path.exists() && config_path.is_dir() {
match fs::read_dir(config_path).await {
Ok(mut entries) => {
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path.extension() == Some(OsStr::new("yaml")) {
match fs::read_to_string(&path).await {
Ok(content) => {
match serde_yaml::from_str::<
mockforge_core::Workspace,
>(
&content
) {
Ok(workspace) => {
if let Err(e) = registry.register_workspace(
workspace.id.clone(),
workspace,
) {
warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
} else {
info!("Auto-registered workspace from {:?}", path);
}
}
Err(e) => {
warn!("Failed to parse workspace from {:?}: {}", path, e);
}
}
}
Err(e) => {
warn!(
"Failed to read workspace file {:?}: {}",
path, e
);
}
}
}
}
}
Err(e) => {
warn!("Failed to read config directory {:?}: {}", config_path, e);
}
}
} else {
warn!(
"Config directory {:?} does not exist or is not a directory",
config_path
);
}
}
}
let registry = Arc::new(registry);
let _workspace_router = WorkspaceRouter::new(registry);
info!("Workspace routing middleware initialized for HTTP server");
}
}
let total_startup_duration = startup_start.elapsed();
info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
app
}
pub async fn build_router_with_auth_and_latency(
spec_path: Option<String>,
_options: Option<()>,
auth_config: Option<mockforge_core::config::AuthConfig>,
latency_injector: Option<LatencyInjector>,
) -> Router {
let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
if let Some(injector) = latency_injector {
let injector = Arc::new(injector);
app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
let injector = injector.clone();
async move {
let _ = injector.inject_latency(&[]).await;
next.run(req).await
}
}));
}
app
}
pub async fn build_router_with_latency(
spec_path: Option<String>,
options: Option<ValidationOptions>,
latency_injector: Option<LatencyInjector>,
) -> Router {
if let Some(spec) = &spec_path {
match OpenApiSpec::from_file(spec).await {
Ok(openapi) => {
let registry = if let Some(opts) = options {
OpenApiRouteRegistry::new_with_options(openapi, opts)
} else {
OpenApiRouteRegistry::new_with_env(openapi)
};
if let Some(injector) = latency_injector {
return registry.build_router_with_latency(injector);
} else {
return registry.build_router();
}
}
Err(e) => {
warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
}
}
}
build_router(None, None, None).await
}
pub async fn build_router_with_auth(
spec_path: Option<String>,
options: Option<ValidationOptions>,
auth_config: Option<mockforge_core::config::AuthConfig>,
) -> Router {
use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
use std::sync::Arc;
#[cfg(feature = "data-faker")]
{
register_core_faker_provider();
}
let spec = if let Some(spec_path) = &spec_path {
match OpenApiSpec::from_file(&spec_path).await {
Ok(spec) => Some(Arc::new(spec)),
Err(e) => {
warn!("Failed to load OpenAPI spec for auth: {}", e);
None
}
}
} else {
None
};
let oauth2_client = if let Some(auth_config) = &auth_config {
if let Some(oauth2_config) = &auth_config.oauth2 {
match create_oauth2_client(oauth2_config) {
Ok(client) => Some(client),
Err(e) => {
warn!("Failed to create OAuth2 client: {}", e);
None
}
}
} else {
None
}
} else {
None
};
let auth_state = AuthState {
config: auth_config.unwrap_or_default(),
spec,
oauth2_client,
introspection_cache: Arc::new(RwLock::new(HashMap::new())),
};
let mut app = Router::new().with_state(auth_state.clone());
if let Some(spec_path) = spec_path {
match OpenApiSpec::from_file(&spec_path).await {
Ok(openapi) => {
info!("Loaded OpenAPI spec from {}", spec_path);
let registry = if let Some(opts) = options {
OpenApiRouteRegistry::new_with_options(openapi, opts)
} else {
OpenApiRouteRegistry::new_with_env(openapi)
};
app = registry.build_router();
}
Err(e) => {
warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
}
}
}
app = app.route(
"/health",
axum::routing::get(|| async {
use mockforge_core::server_utils::health::HealthStatus;
{
match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
Ok(value) => Json(value),
Err(e) => {
tracing::error!("Failed to serialize health status: {}", e);
Json(serde_json::json!({
"status": "healthy",
"service": "mockforge-http",
"uptime_seconds": 0
}))
}
}
}
}),
)
.merge(sse::sse_router())
.merge(file_server::file_serving_router())
.layer(from_fn_with_state(auth_state.clone(), auth_middleware))
.layer(axum::middleware::from_fn(request_logging::log_http_requests));
app
}
pub async fn serve_router(
port: u16,
app: Router,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
serve_router_with_tls(port, app, None).await
}
pub async fn serve_router_with_tls(
port: u16,
app: Router,
tls_config: Option<mockforge_core::config::HttpTlsConfig>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::net::SocketAddr;
let addr = mockforge_core::wildcard_socket_addr(port);
if let Some(ref tls) = tls_config {
if tls.enabled {
info!("HTTPS listening on {}", addr);
return serve_with_tls(addr, app, tls).await;
}
}
info!("HTTP listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
format!(
"Failed to bind HTTP server to port {}: {}\n\
Hint: The port may already be in use. Try using a different port with --http-port or check if another process is using this port with: lsof -i :{} or netstat -tulpn | grep {}",
port, e, port, port
)
})?;
let odata_app = tower::ServiceBuilder::new()
.layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
.service(app);
let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
axum::serve(listener, make_svc).await?;
Ok(())
}
async fn serve_with_tls(
addr: std::net::SocketAddr,
app: Router,
tls_config: &mockforge_core::config::HttpTlsConfig,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use axum_server::tls_rustls::RustlsConfig;
use std::net::SocketAddr;
tls::init_crypto_provider();
info!("Loading TLS configuration for HTTPS server");
let server_config = tls::load_tls_server_config(tls_config)?;
let rustls_config = RustlsConfig::from_config(server_config);
info!("Starting HTTPS server on {}", addr);
let odata_app = tower::ServiceBuilder::new()
.layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
.service(app);
let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
axum_server::bind_rustls(addr, rustls_config)
.serve(make_svc)
.await
.map_err(|e| format!("HTTPS server error: {}", e).into())
}
pub async fn start(
port: u16,
spec_path: Option<String>,
options: Option<ValidationOptions>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
start_with_latency(port, spec_path, options, None).await
}
pub async fn start_with_auth_and_latency(
port: u16,
spec_path: Option<String>,
options: Option<ValidationOptions>,
auth_config: Option<mockforge_core::config::AuthConfig>,
latency_profile: Option<LatencyProfile>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
.await
}
pub async fn start_with_auth_and_injectors(
port: u16,
spec_path: Option<String>,
options: Option<ValidationOptions>,
auth_config: Option<mockforge_core::config::AuthConfig>,
_latency_profile: Option<LatencyProfile>,
_failure_injector: Option<FailureInjector>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let app = build_router_with_auth(spec_path, options, auth_config).await;
serve_router(port, app).await
}
pub async fn start_with_latency(
port: u16,
spec_path: Option<String>,
options: Option<ValidationOptions>,
latency_profile: Option<LatencyProfile>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let latency_injector =
latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
let app = build_router_with_latency(spec_path, options, latency_injector).await;
serve_router(port, app).await
}
pub async fn build_router_with_chains(
spec_path: Option<String>,
options: Option<ValidationOptions>,
circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
) -> Router {
build_router_with_chains_and_multi_tenant(
spec_path,
options,
circling_config,
None,
None,
None,
None,
None,
None,
None,
false,
None, None, None, None, )
.await
}
async fn apply_route_chaos(
injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
method: &http::Method,
uri: &http::Uri,
) -> Option<axum::response::Response> {
use axum::http::StatusCode;
use axum::response::IntoResponse;
if let Some(injector) = injector {
if let Some(fault_response) = injector.get_fault_response(method, uri) {
let mut response = Json(serde_json::json!({
"error": fault_response.error_message,
"fault_type": fault_response.fault_type,
}))
.into_response();
*response.status_mut() = StatusCode::from_u16(fault_response.status_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
return Some(response);
}
if let Err(e) = injector.inject_latency(method, uri).await {
tracing::warn!("Failed to inject latency: {}", e);
}
}
None }
#[allow(clippy::too_many_arguments)]
pub async fn build_router_with_chains_and_multi_tenant(
spec_path: Option<String>,
options: Option<ValidationOptions>,
_circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
cors_config: Option<mockforge_core::config::HttpCorsConfig>,
_ai_generator: Option<Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>>,
smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
traffic_shaping_enabled: bool,
health_manager: Option<Arc<HealthManager>>,
_mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
proxy_config: Option<mockforge_core::proxy::config::ProxyConfig>,
) -> Router {
use crate::latency_profiles::LatencyProfiles;
use crate::op_middleware::Shared;
use mockforge_core::Overrides;
let template_expand =
options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
});
let _shared = Shared {
profiles: LatencyProfiles::default(),
overrides: Overrides::default(),
failure_injector: None,
traffic_shaper,
overrides_enabled: false,
traffic_shaping_enabled,
};
let mut app = Router::new();
let mut include_default_health = true;
let mut captured_routes: Vec<RouteInfo> = Vec::new();
if let Some(ref spec) = spec_path {
match OpenApiSpec::from_file(&spec).await {
Ok(openapi) => {
info!("Loaded OpenAPI spec from {}", spec);
let persona = load_persona_from_config().await;
let mut registry = if let Some(opts) = options {
tracing::debug!("Using custom validation options");
if let Some(ref persona) = persona {
tracing::info!("Using persona '{}' for route generation", persona.name);
}
OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
} else {
tracing::debug!("Using environment-based options");
if let Some(ref persona) = persona {
tracing::info!("Using persona '{}' for route generation", persona.name);
}
OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
};
let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
.unwrap_or_else(|_| "/app/fixtures".to_string());
let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(true);
if custom_fixtures_enabled {
use mockforge_core::CustomFixtureLoader;
use std::path::PathBuf;
use std::sync::Arc;
let fixtures_path = PathBuf::from(&fixtures_dir);
let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
if let Err(e) = custom_loader.load_fixtures().await {
tracing::warn!("Failed to load custom fixtures: {}", e);
} else {
tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
}
}
if registry
.routes()
.iter()
.any(|route| route.method == "GET" && route.path == "/health")
{
include_default_health = false;
}
captured_routes = registry
.routes()
.iter()
.map(|r| RouteInfo {
method: r.method.clone(),
path: r.path.clone(),
operation_id: r.operation.operation_id.clone(),
summary: r.operation.summary.clone(),
description: r.operation.description.clone(),
parameters: r.parameters.clone(),
})
.collect();
{
let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
captured_routes
.iter()
.map(|r| mockforge_core::request_logger::GlobalRouteInfo {
method: r.method.clone(),
path: r.path.clone(),
operation_id: r.operation_id.clone(),
summary: r.summary.clone(),
description: r.description.clone(),
parameters: r.parameters.clone(),
})
.collect();
mockforge_core::request_logger::set_global_routes(global_routes);
tracing::info!("Stored {} routes in global route store", captured_routes.len());
}
let spec_router = if let Some(ref mockai_instance) = _mockai {
tracing::debug!("Building router with MockAI support");
registry.build_router_with_mockai(Some(mockai_instance.clone()))
} else {
registry.build_router()
};
app = app.merge(spec_router);
}
Err(e) => {
warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
}
}
}
let route_chaos_injector: Option<
std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
> = if let Some(ref route_configs) = route_configs {
if !route_configs.is_empty() {
let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
route_configs.to_vec();
match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
Ok(injector) => {
info!(
"Initialized advanced routing features for {} route(s)",
route_configs.len()
);
Some(std::sync::Arc::new(injector)
as std::sync::Arc<
dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
>)
}
Err(e) => {
warn!(
"Failed to initialize advanced routing features: {}. Using basic routing.",
e
);
None
}
}
} else {
None
}
} else {
None
};
if let Some(route_configs) = route_configs {
use axum::http::StatusCode;
use axum::response::IntoResponse;
if !route_configs.is_empty() {
info!("Registering {} custom route(s) from config", route_configs.len());
}
let injector = route_chaos_injector.clone();
for route_config in route_configs {
let status = route_config.response.status;
let body = route_config.response.body.clone();
let headers = route_config.response.headers.clone();
let path = route_config.path.clone();
let method = route_config.method.clone();
let expected_method = method.to_uppercase();
let injector_clone = injector.clone();
app = app.route(
&path,
#[allow(clippy::non_send_fields_in_send_ty)]
axum::routing::any(move |req: http::Request<axum::body::Body>| {
let body = body.clone();
let headers = headers.clone();
let expand = template_expand;
let expected = expected_method.clone();
let status_code = status;
let injector_for_chaos = injector_clone.clone();
async move {
if req.method().as_str() != expected.as_str() {
return axum::response::Response::builder()
.status(StatusCode::METHOD_NOT_ALLOWED)
.header("Allow", &expected)
.body(axum::body::Body::empty())
.unwrap()
.into_response();
}
if let Some(fault_response) = apply_route_chaos(
injector_for_chaos.as_deref(),
req.method(),
req.uri(),
)
.await
{
return fault_response;
}
let mut body_value = body.unwrap_or(serde_json::json!({}));
if expand {
use mockforge_template_expansion::RequestContext;
use serde_json::Value;
use std::collections::HashMap;
let method = req.method().to_string();
let path = req.uri().path().to_string();
let query_params: HashMap<String, Value> = req
.uri()
.query()
.map(|q| {
url::form_urlencoded::parse(q.as_bytes())
.into_owned()
.map(|(k, v)| (k, Value::String(v)))
.collect()
})
.unwrap_or_default();
let headers: HashMap<String, Value> = req
.headers()
.iter()
.map(|(k, v)| {
(
k.to_string(),
Value::String(v.to_str().unwrap_or_default().to_string()),
)
})
.collect();
let context = RequestContext {
method,
path,
query_params,
headers,
body: None, path_params: HashMap::new(),
multipart_fields: HashMap::new(),
multipart_files: HashMap::new(),
};
let body_value_clone = body_value.clone();
let context_clone = context.clone();
body_value = match tokio::task::spawn_blocking(move || {
mockforge_template_expansion::expand_templates_in_json(
body_value_clone,
&context_clone,
)
})
.await
{
Ok(result) => result,
Err(_) => body_value, };
}
let mut response = Json(body_value).into_response();
*response.status_mut() =
StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
for (key, value) in headers {
if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
if let Ok(header_value) = http::HeaderValue::from_str(&value) {
response.headers_mut().insert(header_name, header_value);
}
}
}
response
}
}),
);
debug!("Registered route: {} {}", method, path);
}
}
if let Some(health) = health_manager {
app = app.merge(health::health_router(health));
info!(
"Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
);
} else if include_default_health {
app = app.route(
"/health",
axum::routing::get(|| async {
use mockforge_core::server_utils::health::HealthStatus;
{
match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
Ok(value) => Json(value),
Err(e) => {
tracing::error!("Failed to serialize health status: {}", e);
Json(serde_json::json!({
"status": "healthy",
"service": "mockforge-http",
"uptime_seconds": 0
}))
}
}
}
}),
);
}
app = app.merge(sse::sse_router());
app = app.merge(file_server::file_serving_router());
let mgmt_spec = if let Some(ref sp) = spec_path {
match OpenApiSpec::from_file(sp).await {
Ok(s) => Some(Arc::new(s)),
Err(e) => {
debug!("Failed to load OpenAPI spec for management API: {}", e);
None
}
}
} else {
None
};
let spec_path_clone = spec_path.clone();
let mgmt_port = std::env::var("PORT")
.or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(3000);
let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
use std::sync::Arc;
let ws_state = WsManagementState::new();
let ws_broadcast = Arc::new(ws_state.tx.clone());
let management_state = management_state.with_ws_broadcast(ws_broadcast);
let management_state = if let Some(proxy_cfg) = proxy_config {
use tokio::sync::RwLock;
let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
management_state.with_proxy_config(proxy_config_arc)
} else {
management_state
};
#[cfg(feature = "smtp")]
let management_state = {
if let Some(smtp_reg) = smtp_registry {
match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
Err(e) => {
error!(
"Invalid SMTP registry type passed to HTTP management state: {:?}",
e.type_id()
);
management_state
}
}
} else {
management_state
}
};
#[cfg(not(feature = "smtp"))]
let management_state = {
let _ = smtp_registry;
management_state
};
#[cfg(feature = "mqtt")]
let management_state = {
if let Some(broker) = mqtt_broker {
match broker.downcast::<mockforge_mqtt::MqttBroker>() {
Ok(broker) => management_state.with_mqtt_broker(broker),
Err(e) => {
error!(
"Invalid MQTT broker passed to HTTP management state: {:?}",
e.type_id()
);
management_state
}
}
} else {
management_state
}
};
#[cfg(not(feature = "mqtt"))]
let management_state = {
let _ = mqtt_broker;
management_state
};
app = app.nest("/__mockforge/api", management_router(management_state));
app = app.merge(verification_router());
use crate::auth::oidc::oidc_router;
app = app.merge(oidc_router());
{
use mockforge_core::security::get_global_access_review_service;
if let Some(service) = get_global_access_review_service().await {
use crate::handlers::access_review::{access_review_router, AccessReviewState};
let review_state = AccessReviewState { service };
app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
debug!("Access review API mounted at /api/v1/security/access-reviews");
}
}
{
use mockforge_core::security::get_global_privileged_access_manager;
if let Some(manager) = get_global_privileged_access_manager().await {
use crate::handlers::privileged_access::{
privileged_access_router, PrivilegedAccessState,
};
let privileged_state = PrivilegedAccessState { manager };
app = app.nest(
"/api/v1/security/privileged-access",
privileged_access_router(privileged_state),
);
debug!("Privileged access API mounted at /api/v1/security/privileged-access");
}
}
{
use mockforge_core::security::get_global_change_management_engine;
if let Some(engine) = get_global_change_management_engine().await {
use crate::handlers::change_management::{
change_management_router, ChangeManagementState,
};
let change_state = ChangeManagementState { engine };
app = app.nest("/api/v1/change-management", change_management_router(change_state));
debug!("Change management API mounted at /api/v1/change-management");
}
}
{
use mockforge_core::security::get_global_risk_assessment_engine;
if let Some(engine) = get_global_risk_assessment_engine().await {
use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
let risk_state = RiskAssessmentState { engine };
app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
debug!("Risk assessment API mounted at /api/v1/security/risks");
}
}
{
use crate::auth::token_lifecycle::TokenLifecycleManager;
use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
let lifecycle_state = TokenLifecycleState {
manager: lifecycle_manager,
};
app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
debug!("Token lifecycle API mounted at /api/v1/auth");
}
{
use crate::auth::oidc::load_oidc_state;
use crate::auth::token_lifecycle::TokenLifecycleManager;
use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
let oauth2_state = OAuth2ServerState {
oidc_state,
lifecycle_manager,
auth_codes: Arc::new(RwLock::new(HashMap::new())),
refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
};
app = app.merge(oauth2_server_router(oauth2_state));
debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
}
{
use crate::auth::oidc::load_oidc_state;
use crate::auth::risk_engine::RiskEngine;
use crate::auth::token_lifecycle::TokenLifecycleManager;
use crate::handlers::consent::{consent_router, ConsentState};
use crate::handlers::oauth2_server::OAuth2ServerState;
let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
let oauth2_state = OAuth2ServerState {
oidc_state: oidc_state.clone(),
lifecycle_manager: lifecycle_manager.clone(),
auth_codes: Arc::new(RwLock::new(HashMap::new())),
refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
};
let risk_engine = Arc::new(RiskEngine::default());
let consent_state = ConsentState {
oauth2_state,
risk_engine,
};
app = app.merge(consent_router(consent_state));
debug!("Consent screen endpoints mounted at /consent");
}
{
use crate::auth::risk_engine::RiskEngine;
use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
let risk_engine = Arc::new(RiskEngine::default());
let risk_state = RiskSimulationState { risk_engine };
app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
debug!("Risk simulation API mounted at /api/v1/auth/risk");
}
let database = {
use crate::database::Database;
let database_url = std::env::var("DATABASE_URL").ok();
match Database::connect_optional(database_url.as_deref()).await {
Ok(db) => {
if db.is_connected() {
if let Err(e) = db.migrate_if_connected().await {
warn!("Failed to run database migrations: {}", e);
} else {
info!("Database connected and migrations applied");
}
}
Some(db)
}
Err(e) => {
warn!("Failed to connect to database: {}. Continuing without database support.", e);
None
}
}
};
let (drift_engine, incident_manager, drift_config) = {
use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
use mockforge_core::incidents::{IncidentManager, IncidentStore};
use std::sync::Arc;
let drift_config = DriftBudgetConfig::default();
let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
let incident_store = Arc::new(IncidentStore::default());
let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
(drift_engine, incident_manager, drift_config)
};
{
use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
use crate::middleware::drift_tracking::DriftTrackingState;
use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
use mockforge_core::consumer_contracts::{ConsumerBreakingChangeDetector, UsageRecorder};
use std::sync::Arc;
let usage_recorder = Arc::new(UsageRecorder::default());
let consumer_detector =
Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
let diff_analyzer = if drift_config.enabled {
match ContractDiffAnalyzer::new(
mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
) {
Ok(analyzer) => Some(Arc::new(analyzer)),
Err(e) => {
warn!("Failed to create contract diff analyzer: {}", e);
None
}
}
} else {
None
};
let spec = if let Some(ref spec_path) = spec_path {
match OpenApiSpec::from_file(spec_path).await {
Ok(s) => Some(Arc::new(s)),
Err(e) => {
debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
None
}
}
} else {
None
};
let drift_tracking_state = DriftTrackingState {
diff_analyzer,
spec,
drift_engine: drift_engine.clone(),
incident_manager: incident_manager.clone(),
usage_recorder,
consumer_detector,
enabled: drift_config.enabled,
};
app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
let drift_tracking_state_clone = drift_tracking_state.clone();
app = app.layer(axum::middleware::from_fn(
move |mut req: axum::extract::Request, next: axum::middleware::Next| {
let state = drift_tracking_state_clone.clone();
async move {
if req.extensions().get::<DriftTrackingState>().is_none() {
req.extensions_mut().insert(state);
}
middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
.await
}
},
));
let drift_state = DriftBudgetState {
engine: drift_engine.clone(),
incident_manager: incident_manager.clone(),
gitops_handler: None, };
app = app.merge(drift_budget_router(drift_state));
debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
}
#[cfg(feature = "pipelines")]
{
use crate::handlers::pipelines::{pipeline_router, PipelineState};
let pipeline_state = PipelineState::new();
app = app.merge(pipeline_router(pipeline_state));
debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
}
{
use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
use crate::handlers::forecasting::{forecasting_router, ForecastingState};
use crate::handlers::semantic_drift::{semantic_drift_router, SemanticDriftState};
use crate::handlers::threat_modeling::{threat_modeling_router, ThreatModelingState};
use mockforge_core::contract_drift::forecasting::{Forecaster, ForecastingConfig};
use mockforge_core::contract_drift::threat_modeling::{
ThreatAnalyzer, ThreatModelingConfig,
};
use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
use std::sync::Arc;
let forecasting_config = ForecastingConfig::default();
let forecaster = Arc::new(Forecaster::new(forecasting_config));
let forecasting_state = ForecastingState {
forecaster,
database: database.clone(),
};
let semantic_manager = Arc::new(SemanticIncidentManager::new());
let semantic_state = SemanticDriftState {
manager: semantic_manager,
database: database.clone(),
};
let threat_config = ThreatModelingConfig::default();
let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
Ok(analyzer) => Arc::new(analyzer),
Err(e) => {
warn!("Failed to create threat analyzer: {}. Using default.", e);
Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
|_| {
ThreatAnalyzer::new(ThreatModelingConfig {
enabled: false,
..Default::default()
})
.expect("Failed to create fallback threat analyzer")
},
))
}
};
let mut webhook_configs = Vec::new();
let config_paths = [
"config.yaml",
"mockforge.yaml",
"tools/mockforge/config.yaml",
"../tools/mockforge/config.yaml",
];
for path in &config_paths {
if let Ok(config) = mockforge_core::config::load_config(path).await {
if !config.incidents.webhooks.is_empty() {
webhook_configs = config.incidents.webhooks.clone();
info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
break;
}
}
}
if webhook_configs.is_empty() {
debug!("No webhook configs found in config files, using empty list");
}
let threat_state = ThreatModelingState {
analyzer: threat_analyzer,
webhook_configs,
database: database.clone(),
};
let contract_health_state = ContractHealthState {
incident_manager: incident_manager.clone(),
semantic_manager: Arc::new(SemanticIncidentManager::new()),
database: database.clone(),
};
app = app.merge(forecasting_router(forecasting_state));
debug!("Forecasting endpoints mounted at /api/v1/forecasts");
app = app.merge(semantic_drift_router(semantic_state));
debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
app = app.merge(threat_modeling_router(threat_state));
debug!("Threat modeling endpoints mounted at /api/v1/threats");
app = app.merge(contract_health_router(contract_health_state));
debug!("Contract health endpoints mounted at /api/v1/contract-health");
}
{
use crate::handlers::protocol_contracts::{
protocol_contracts_router, ProtocolContractState,
};
use mockforge_core::contract_drift::{
ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
};
use std::sync::Arc;
use tokio::sync::RwLock;
let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
let mut fitness_registry = FitnessFunctionRegistry::new();
let config_paths = [
"config.yaml",
"mockforge.yaml",
"tools/mockforge/config.yaml",
"../tools/mockforge/config.yaml",
];
let mut config_loaded = false;
for path in &config_paths {
if let Ok(config) = mockforge_core::config::load_config(path).await {
if !config.contracts.fitness_rules.is_empty() {
if let Err(e) =
fitness_registry.load_from_config(&config.contracts.fitness_rules)
{
warn!("Failed to load fitness rules from config {}: {}", path, e);
} else {
info!(
"Loaded {} fitness rules from config: {}",
config.contracts.fitness_rules.len(),
path
);
config_loaded = true;
break;
}
}
}
}
if !config_loaded {
debug!("No fitness rules found in config files, using empty registry");
}
let fitness_registry = Arc::new(RwLock::new(fitness_registry));
let consumer_mapping_registry =
mockforge_core::contract_drift::ConsumerMappingRegistry::new();
let consumer_analyzer =
Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
let protocol_state = ProtocolContractState {
registry: contract_registry,
drift_engine: Some(drift_engine.clone()),
incident_manager: Some(incident_manager.clone()),
fitness_registry: Some(fitness_registry),
consumer_analyzer: Some(consumer_analyzer),
};
app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
}
#[cfg(feature = "behavioral-cloning")]
{
use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
use std::path::PathBuf;
let db_path = std::env::var("RECORDER_DATABASE_PATH")
.ok()
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
let bc_middleware_state = if let Some(path) = db_path {
BehavioralCloningMiddlewareState::with_database_path(path)
} else {
BehavioralCloningMiddlewareState::new()
};
let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
.ok()
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(false);
if enabled {
let bc_state_clone = bc_middleware_state.clone();
app = app.layer(axum::middleware::from_fn(
move |mut req: axum::extract::Request, next: axum::middleware::Next| {
let state = bc_state_clone.clone();
async move {
if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
req.extensions_mut().insert(state);
}
crate::middleware::behavioral_cloning::behavioral_cloning_middleware(
req, next,
)
.await
}
},
));
debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
}
}
{
use crate::handlers::consumer_contracts::{
consumer_contracts_router, ConsumerContractsState,
};
use mockforge_core::consumer_contracts::{
ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
};
use std::sync::Arc;
let registry = Arc::new(ConsumerRegistry::default());
let usage_recorder = Arc::new(UsageRecorder::default());
let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
let consumer_state = ConsumerContractsState {
registry,
usage_recorder,
detector,
violations: Arc::new(RwLock::new(HashMap::new())),
};
app = app.merge(consumer_contracts_router(consumer_state));
debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
}
#[cfg(feature = "behavioral-cloning")]
{
use crate::handlers::behavioral_cloning::{
behavioral_cloning_router, BehavioralCloningState,
};
use std::path::PathBuf;
let db_path = std::env::var("RECORDER_DATABASE_PATH")
.ok()
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
let bc_state = if let Some(path) = db_path {
BehavioralCloningState::with_database_path(path)
} else {
BehavioralCloningState::new()
};
app = app.merge(behavioral_cloning_router(bc_state));
debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
}
{
use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
use crate::handlers::consistency::{consistency_router, ConsistencyState};
use mockforge_core::consistency::ConsistencyEngine;
use std::sync::Arc;
let consistency_engine = Arc::new(ConsistencyEngine::new());
let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
consistency_engine.register_adapter(http_adapter.clone()).await;
let consistency_state = ConsistencyState {
engine: consistency_engine.clone(),
};
use crate::handlers::xray::XRayState;
let xray_state = Arc::new(XRayState {
engine: consistency_engine.clone(),
request_contexts: std::sync::Arc::new(RwLock::new(HashMap::new())),
});
let consistency_middleware_state = ConsistencyMiddlewareState {
engine: consistency_engine.clone(),
adapter: http_adapter,
xray_state: Some(xray_state.clone()),
};
let consistency_middleware_state_clone = consistency_middleware_state.clone();
app = app.layer(axum::middleware::from_fn(
move |mut req: axum::extract::Request, next: axum::middleware::Next| {
let state = consistency_middleware_state_clone.clone();
async move {
if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
req.extensions_mut().insert(state);
}
consistency::middleware::consistency_middleware(req, next).await
}
},
));
app = app.merge(consistency_router(consistency_state));
debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
{
use crate::handlers::fidelity::{fidelity_router, FidelityState};
let fidelity_state = FidelityState::new();
app = app.merge(fidelity_router(fidelity_state));
debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
}
{
use crate::handlers::scenario_studio::{scenario_studio_router, ScenarioStudioState};
let scenario_studio_state = ScenarioStudioState::new();
app = app.merge(scenario_studio_router(scenario_studio_state));
debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
}
{
use crate::handlers::performance::{performance_router, PerformanceState};
let performance_state = PerformanceState::new();
app = app.nest("/api/performance", performance_router(performance_state));
debug!("Performance mode endpoints mounted at /api/performance");
}
{
use crate::handlers::world_state::{world_state_router, WorldStateState};
use mockforge_world_state::WorldStateEngine;
use std::sync::Arc;
use tokio::sync::RwLock;
let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
let world_state_state = WorldStateState {
engine: world_state_engine,
};
app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
debug!("World state endpoints mounted at /api/world-state");
}
{
use crate::handlers::snapshots::{snapshot_router, SnapshotState};
use mockforge_core::snapshots::SnapshotManager;
use std::path::PathBuf;
let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
let snapshot_state = SnapshotState {
manager: snapshot_manager,
consistency_engine: Some(consistency_engine.clone()),
workspace_persistence: None, vbr_engine: None, recorder: None, };
app = app.merge(snapshot_router(snapshot_state));
debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
{
use crate::handlers::xray::xray_router;
app = app.merge(xray_router((*xray_state).clone()));
debug!("X-Ray API endpoints mounted at /api/v1/xray");
}
}
{
use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
use crate::middleware::ab_testing::ab_testing_middleware;
let ab_testing_state = ABTestingState::new();
let ab_testing_state_clone = ab_testing_state.clone();
app = app.layer(axum::middleware::from_fn(
move |mut req: axum::extract::Request, next: axum::middleware::Next| {
let state = ab_testing_state_clone.clone();
async move {
if req.extensions().get::<ABTestingState>().is_none() {
req.extensions_mut().insert(state);
}
ab_testing_middleware(req, next).await
}
},
));
app = app.merge(ab_testing_router(ab_testing_state));
debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
}
}
{
use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
use mockforge_core::pr_generation::{PRGenerator, PRProvider};
use std::sync::Arc;
let pr_config = mockforge_core::pr_generation::PRGenerationConfig::from_env();
let generator = if pr_config.enabled && pr_config.token.is_some() {
let token = pr_config.token.as_ref().unwrap().clone();
let generator = match pr_config.provider {
PRProvider::GitHub => PRGenerator::new_github(
pr_config.owner.clone(),
pr_config.repo.clone(),
token,
pr_config.base_branch.clone(),
),
PRProvider::GitLab => PRGenerator::new_gitlab(
pr_config.owner.clone(),
pr_config.repo.clone(),
token,
pr_config.base_branch.clone(),
),
};
Some(Arc::new(generator))
} else {
None
};
let pr_state = PRGenerationState {
generator: generator.clone(),
};
app = app.merge(pr_generation_router(pr_state));
if generator.is_some() {
debug!(
"PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
pr_config.provider
);
} else {
debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
}
}
app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
if let Some(mt_config) = multi_tenant_config {
if mt_config.enabled {
use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
use std::sync::Arc;
info!(
"Multi-tenant mode enabled with {} routing strategy",
match mt_config.routing_strategy {
mockforge_core::RoutingStrategy::Path => "path-based",
mockforge_core::RoutingStrategy::Port => "port-based",
mockforge_core::RoutingStrategy::Both => "hybrid",
}
);
let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
let default_workspace =
mockforge_core::Workspace::new(mt_config.default_workspace.clone());
if let Err(e) =
registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
{
warn!("Failed to register default workspace: {}", e);
} else {
info!("Registered default workspace: '{}'", mt_config.default_workspace);
}
let registry = Arc::new(registry);
let _workspace_router = WorkspaceRouter::new(registry);
info!("Workspace routing middleware initialized for HTTP server");
}
}
let mut final_cors_config = cors_config;
let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
let mut rate_limit_config = middleware::RateLimitConfig {
requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1000),
burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2000),
per_ip: true,
per_endpoint: false,
};
if let Some(deploy_config) = &deceptive_deploy_config {
if deploy_config.enabled {
info!("Deceptive deploy mode enabled - applying production-like configuration");
if let Some(prod_cors) = &deploy_config.cors {
final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
enabled: true,
allowed_origins: prod_cors.allowed_origins.clone(),
allowed_methods: prod_cors.allowed_methods.clone(),
allowed_headers: prod_cors.allowed_headers.clone(),
allow_credentials: prod_cors.allow_credentials,
});
info!("Applied production-like CORS configuration");
}
if let Some(prod_rate_limit) = &deploy_config.rate_limit {
rate_limit_config = middleware::RateLimitConfig {
requests_per_minute: prod_rate_limit.requests_per_minute,
burst: prod_rate_limit.burst,
per_ip: prod_rate_limit.per_ip,
per_endpoint: false,
};
info!(
"Applied production-like rate limiting: {} req/min, burst: {}",
prod_rate_limit.requests_per_minute, prod_rate_limit.burst
);
}
if !deploy_config.headers.is_empty() {
let headers_map: HashMap<String, String> = deploy_config.headers.clone();
production_headers = Some(std::sync::Arc::new(headers_map));
info!("Configured {} production headers", deploy_config.headers.len());
}
if let Some(prod_oauth) = &deploy_config.oauth {
let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
oauth2: Some(oauth2_config),
..Default::default()
});
info!("Applied production-like OAuth configuration for deceptive deploy");
}
}
}
let rate_limiter =
std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
if let Some(headers) = production_headers.clone() {
state = state.with_production_headers(headers);
}
app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
if state.production_headers.is_some() {
app =
app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
}
if let Some(auth_config) = deceptive_deploy_auth_config {
use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
match create_oauth2_client(oauth2_config) {
Ok(client) => Some(client),
Err(e) => {
warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
None
}
}
} else {
None
};
let auth_state = AuthState {
config: auth_config,
spec: None, oauth2_client,
introspection_cache: Arc::new(RwLock::new(HashMap::new())),
};
app = app.layer(from_fn_with_state(auth_state, auth_middleware));
info!("Applied OAuth authentication middleware from deceptive deploy configuration");
}
#[cfg(feature = "runtime-daemon")]
{
use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
use std::sync::Arc;
let daemon_config = RuntimeDaemonConfig::from_env();
if daemon_config.enabled {
info!("Runtime daemon enabled - auto-creating mocks from 404s");
let management_api_url =
std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
let port =
std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
format!("http://localhost:{}", port)
});
let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
let detector = NotFoundDetector::new(daemon_config.clone());
detector.set_generator(generator).await;
let detector_clone = detector.clone();
app = app.layer(axum::middleware::from_fn(
move |req: axum::extract::Request, next: axum::middleware::Next| {
let detector = detector_clone.clone();
async move { detector.detect_and_auto_create(req, next).await }
},
));
debug!("Runtime daemon 404 detection middleware added");
}
}
{
let routes_state = HttpServerState::with_routes(captured_routes);
let routes_router = Router::new()
.route("/__mockforge/routes", axum::routing::get(get_routes_handler))
.with_state(routes_state);
app = app.merge(routes_router);
}
app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
app = apply_cors_middleware(app, final_cors_config);
app
}
#[test]
fn test_route_info_clone() {
let route = RouteInfo {
method: "POST".to_string(),
path: "/users".to_string(),
operation_id: Some("createUser".to_string()),
summary: None,
description: None,
parameters: vec![],
};
let cloned = route.clone();
assert_eq!(route.method, cloned.method);
assert_eq!(route.path, cloned.path);
assert_eq!(route.operation_id, cloned.operation_id);
}
#[test]
fn test_http_server_state_new() {
let state = HttpServerState::new();
assert_eq!(state.routes.len(), 0);
}
#[test]
fn test_http_server_state_with_routes() {
let routes = vec![
RouteInfo {
method: "GET".to_string(),
path: "/users".to_string(),
operation_id: Some("getUsers".to_string()),
summary: None,
description: None,
parameters: vec![],
},
RouteInfo {
method: "POST".to_string(),
path: "/users".to_string(),
operation_id: Some("createUser".to_string()),
summary: None,
description: None,
parameters: vec![],
},
];
let state = HttpServerState::with_routes(routes.clone());
assert_eq!(state.routes.len(), 2);
assert_eq!(state.routes[0].method, "GET");
assert_eq!(state.routes[1].method, "POST");
}
#[test]
fn test_http_server_state_clone() {
let routes = vec![RouteInfo {
method: "GET".to_string(),
path: "/test".to_string(),
operation_id: None,
summary: None,
description: None,
parameters: vec![],
}];
let state = HttpServerState::with_routes(routes);
let cloned = state.clone();
assert_eq!(state.routes.len(), cloned.routes.len());
assert_eq!(state.routes[0].method, cloned.routes[0].method);
}
#[tokio::test]
async fn test_build_router_without_openapi() {
let _router = build_router(None, None, None).await;
}
#[tokio::test]
async fn test_build_router_with_nonexistent_spec() {
let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
}
#[tokio::test]
async fn test_build_router_with_auth_and_latency() {
let _router = build_router_with_auth_and_latency(None, None, None, None).await;
}
#[tokio::test]
async fn test_build_router_with_latency() {
let _router = build_router_with_latency(None, None, None).await;
}
#[tokio::test]
async fn test_build_router_with_auth() {
let _router = build_router_with_auth(None, None, None).await;
}
#[tokio::test]
async fn test_build_router_with_chains() {
let _router = build_router_with_chains(None, None, None).await;
}
#[test]
fn test_route_info_with_all_fields() {
let route = RouteInfo {
method: "PUT".to_string(),
path: "/users/{id}".to_string(),
operation_id: Some("updateUser".to_string()),
summary: Some("Update user".to_string()),
description: Some("Updates an existing user".to_string()),
parameters: vec!["id".to_string(), "body".to_string()],
};
assert!(route.operation_id.is_some());
assert!(route.summary.is_some());
assert!(route.description.is_some());
assert_eq!(route.parameters.len(), 2);
}
#[test]
fn test_route_info_with_minimal_fields() {
let route = RouteInfo {
method: "DELETE".to_string(),
path: "/users/{id}".to_string(),
operation_id: None,
summary: None,
description: None,
parameters: vec![],
};
assert!(route.operation_id.is_none());
assert!(route.summary.is_none());
assert!(route.description.is_none());
assert_eq!(route.parameters.len(), 0);
}
#[test]
fn test_http_server_state_empty_routes() {
let state = HttpServerState::with_routes(vec![]);
assert_eq!(state.routes.len(), 0);
}
#[test]
fn test_http_server_state_multiple_routes() {
let routes = vec![
RouteInfo {
method: "GET".to_string(),
path: "/users".to_string(),
operation_id: Some("listUsers".to_string()),
summary: Some("List all users".to_string()),
description: None,
parameters: vec![],
},
RouteInfo {
method: "GET".to_string(),
path: "/users/{id}".to_string(),
operation_id: Some("getUser".to_string()),
summary: Some("Get a user".to_string()),
description: None,
parameters: vec!["id".to_string()],
},
RouteInfo {
method: "POST".to_string(),
path: "/users".to_string(),
operation_id: Some("createUser".to_string()),
summary: Some("Create a user".to_string()),
description: None,
parameters: vec!["body".to_string()],
},
];
let state = HttpServerState::with_routes(routes);
assert_eq!(state.routes.len(), 3);
let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
assert!(methods.contains(&"GET"));
assert!(methods.contains(&"POST"));
}
#[test]
fn test_http_server_state_with_rate_limiter() {
use std::sync::Arc;
let config = crate::middleware::RateLimitConfig::default();
let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
let state = HttpServerState::new().with_rate_limiter(rate_limiter);
assert!(state.rate_limiter.is_some());
assert_eq!(state.routes.len(), 0);
}
#[tokio::test]
async fn test_build_router_includes_rate_limiter() {
let _router = build_router(None, None, None).await;
}