use axum::{
routing::{delete, get, post},
Router,
};
use tower_http::{
compression::CompressionLayer,
cors::{AllowOrigin, Any, CorsLayer},
};
use crate::audit::init_global_audit_store;
use crate::auth::init_global_user_store;
use crate::handlers::analytics::AnalyticsState;
use crate::handlers::AdminState;
use crate::handlers::*;
use crate::rbac::rbac_middleware;
use crate::time_travel_handlers;
use axum::middleware::from_fn;
use mockforge_core::{get_global_logger, init_global_logger};
#[allow(clippy::too_many_arguments)]
pub fn create_admin_router(
http_server_addr: Option<std::net::SocketAddr>,
ws_server_addr: Option<std::net::SocketAddr>,
grpc_server_addr: Option<std::net::SocketAddr>,
graphql_server_addr: Option<std::net::SocketAddr>,
api_enabled: bool,
admin_port: u16,
prometheus_url: String,
chaos_api_state: Option<std::sync::Arc<mockforge_chaos::api::ChaosApiState>>,
latency_injector: Option<
std::sync::Arc<tokio::sync::RwLock<mockforge_core::latency::LatencyInjector>>,
>,
mockai: Option<
std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
>,
continuum_config: Option<mockforge_core::ContinuumConfig>,
virtual_clock: Option<std::sync::Arc<mockforge_core::VirtualClock>>,
recorder: Option<std::sync::Arc<mockforge_recorder::Recorder>>,
federation: Option<std::sync::Arc<mockforge_federation::Federation>>,
vbr_engine: Option<std::sync::Arc<mockforge_vbr::VbrEngine>>,
) -> Router {
let _logger = get_global_logger().unwrap_or_else(|| init_global_logger(1000));
let _audit_store = init_global_audit_store(10000);
let _user_store = init_global_user_store();
let cors = match std::env::var("CORS_ALLOWED_ORIGINS") {
Ok(origins) if !origins.is_empty() => {
let allowed_origins: Vec<_> =
origins.split(',').filter_map(|s| s.trim().parse().ok()).collect();
tracing::info!(
"Admin UI CORS configured with {} allowed origins",
allowed_origins.len()
);
CorsLayer::new()
.allow_origin(AllowOrigin::list(allowed_origins))
.allow_methods(Any)
.allow_headers(Any)
}
_ => {
tracing::info!("Admin UI CORS configured with strict same-origin policy");
CorsLayer::new()
.allow_origin(AllowOrigin::exact(
"null".parse().expect("'null' is a valid header value"),
))
.allow_methods(Any)
.allow_headers(Any)
}
};
let state = AdminState::new(
http_server_addr,
ws_server_addr,
grpc_server_addr,
graphql_server_addr,
api_enabled,
admin_port,
chaos_api_state,
latency_injector,
mockai,
continuum_config,
virtual_clock,
recorder,
federation,
vbr_engine,
);
let state_clone = state.clone();
tokio::spawn(async move {
state_clone.start_system_monitoring().await;
});
let mut router = Router::new()
.route("/", get(serve_admin_html))
.route("/assets/index.css", get(serve_admin_css))
.route("/assets/index.js", get(serve_admin_js))
.route("/assets/{filename}", get(serve_vendor_asset))
.route("/api-docs", get(serve_api_docs))
.route("/mockforge-icon.png", get(serve_icon))
.route("/mockforge-icon-32.png", get(serve_icon_32))
.route("/mockforge-icon-48.png", get(serve_icon_48))
.route("/mockforge-logo.png", get(serve_logo))
.route("/mockforge-logo-40.png", get(serve_logo_40))
.route("/mockforge-logo-80.png", get(serve_logo_80))
.route("/manifest.json", get(serve_manifest))
.route("/sw.js", get(serve_service_worker))
.route("/__mockforge/auth/login", post(crate::auth::login))
.route("/__mockforge/auth/refresh", post(crate::auth::refresh_token))
.route("/__mockforge/auth/logout", post(crate::auth::logout))
.route("/__mockforge/auth/me", get(crate::auth::get_current_user))
.route("/__mockforge/health", get(get_health));
router = router
.route("/__mockforge/dashboard", get(get_dashboard))
.route("/_mf", get(get_dashboard)) .route("/admin/server-info", get(get_server_info))
.route("/__mockforge/server-info", get(get_server_info))
.route("/__mockforge/routes", get(get_routes))
.route("/__mockforge/logs", get(get_logs))
.route("/__mockforge/logs/sse", get(logs_sse))
.route("/__mockforge/metrics", get(get_metrics))
.route("/__mockforge/api/reality/trace/{request_id}", get(get_reality_trace))
.route("/__mockforge/api/reality/response-trace/{request_id}", get(get_response_trace))
.route("/__mockforge/config", get(get_config))
.route("/__mockforge/config/latency", post(update_latency))
.route("/__mockforge/config/faults", post(update_faults))
.route("/__mockforge/config/proxy", post(update_proxy))
.route("/__mockforge/config/traffic-shaping", post(update_traffic_shaping))
.route("/__mockforge/logs", delete(clear_logs))
.route("/__mockforge/restart", post(restart_servers))
.route("/__mockforge/restart/status", get(get_restart_status))
.route("/__mockforge/fixtures", get(get_fixtures))
.route("/__mockforge/fixtures/{id}", delete(delete_fixture))
.route("/__mockforge/fixtures/bulk", delete(delete_fixtures_bulk))
.route("/__mockforge/audit/logs", get(get_audit_logs))
.route("/__mockforge/audit/stats", get(get_audit_stats))
.route("/__mockforge/fixtures/{id}/download", get(download_fixture))
.route("/__mockforge/fixtures/{id}/rename", post(rename_fixture))
.route("/__mockforge/fixtures/{id}/move", post(move_fixture))
.route("/__mockforge/import/postman", post(import_postman))
.route("/__mockforge/import/insomnia", post(import_insomnia))
.route("/__mockforge/import/curl", post(import_curl))
.route("/__mockforge/import/preview", post(preview_import))
.route("/__mockforge/import/history", get(get_import_history))
.route("/__mockforge/import/history/clear", post(clear_import_history))
.route("/__mockforge/plugins", get(get_plugins))
.route("/__mockforge/plugins/status", get(get_plugin_status))
.route("/__mockforge/plugins/{id}", get(get_plugin_details))
.route("/__mockforge/plugins/{id}", delete(delete_plugin))
.route("/__mockforge/plugins/install", post(install_plugin))
.route("/__mockforge/plugins/validate", post(validate_plugin))
.route("/api/plugins/install", post(install_plugin))
.route("/__mockforge/plugins/reload", post(reload_plugin))
.route("/__mockforge/chains", get(proxy_chains_list))
.route("/__mockforge/chains", post(proxy_chains_create))
.route("/__mockforge/chains/{id}", get(proxy_chain_get))
.route("/__mockforge/chains/{id}", axum::routing::put(proxy_chain_update))
.route("/__mockforge/chains/{id}", delete(proxy_chain_delete))
.route("/__mockforge/chains/{id}/execute", post(proxy_chain_execute))
.route("/__mockforge/chains/{id}/validate", post(proxy_chain_validate))
.route("/__mockforge/chains/{id}/history", get(proxy_chain_history))
.route("/__mockforge/graph", get(get_graph))
.route("/__mockforge/graph/sse", get(graph_sse))
.route("/__mockforge/validation", get(get_validation))
.route("/__mockforge/validation", post(update_validation))
.route("/__mockforge/migration/routes", get(get_migration_routes))
.route("/__mockforge/migration/routes/{pattern}/toggle", post(toggle_route_migration))
.route("/__mockforge/migration/routes/{pattern}", axum::routing::put(set_route_migration_mode))
.route("/__mockforge/migration/groups/{group}/toggle", post(toggle_group_migration))
.route("/__mockforge/migration/groups/{group}", axum::routing::put(set_group_migration_mode))
.route("/__mockforge/migration/groups", get(get_migration_groups))
.route("/__mockforge/migration/status", get(get_migration_status))
.route("/__mockforge/env", get(get_env_vars))
.route("/__mockforge/env", post(update_env_var))
.route("/__mockforge/files/content", post(get_file_content))
.route("/__mockforge/files/save", post(save_file_content))
.route("/__mockforge/smoke", get(get_smoke_tests))
.route("/__mockforge/smoke/run", get(run_smoke_tests_endpoint))
.route("/__mockforge/time-travel/status", get(time_travel_handlers::get_time_travel_status))
.route("/__mockforge/time-travel/enable", post(time_travel_handlers::enable_time_travel))
.route("/__mockforge/time-travel/disable", post(time_travel_handlers::disable_time_travel))
.route("/__mockforge/time-travel/advance", post(time_travel_handlers::advance_time))
.route("/__mockforge/time-travel/set", post(time_travel_handlers::set_time))
.route("/__mockforge/time-travel/scale", post(time_travel_handlers::set_time_scale))
.route("/__mockforge/time-travel/reset", post(time_travel_handlers::reset_time_travel))
.route("/__mockforge/time-travel/schedule", post(time_travel_handlers::schedule_response))
.route("/__mockforge/time-travel/scheduled", get(time_travel_handlers::list_scheduled_responses))
.route("/__mockforge/time-travel/scheduled/{id}", delete(time_travel_handlers::cancel_scheduled_response))
.route("/__mockforge/time-travel/scheduled/clear", post(time_travel_handlers::clear_scheduled_responses))
.route("/__mockforge/time-travel/scenario/save", post(time_travel_handlers::save_scenario))
.route("/__mockforge/time-travel/scenario/load", post(time_travel_handlers::load_scenario))
.route("/__mockforge/time-travel/cron", get(time_travel_handlers::list_cron_jobs))
.route("/__mockforge/time-travel/cron", post(time_travel_handlers::create_cron_job))
.route("/__mockforge/time-travel/cron/{id}", get(time_travel_handlers::get_cron_job))
.route("/__mockforge/time-travel/cron/{id}", delete(time_travel_handlers::delete_cron_job))
.route("/__mockforge/time-travel/cron/{id}/enable", post(time_travel_handlers::set_cron_job_enabled))
.route("/__mockforge/time-travel/mutations", get(time_travel_handlers::list_mutation_rules))
.route("/__mockforge/time-travel/mutations", post(time_travel_handlers::create_mutation_rule))
.route("/__mockforge/time-travel/mutations/{id}", get(time_travel_handlers::get_mutation_rule))
.route("/__mockforge/time-travel/mutations/{id}", delete(time_travel_handlers::delete_mutation_rule))
.route("/__mockforge/time-travel/mutations/{id}/enable", post(time_travel_handlers::set_mutation_rule_enabled))
.route("/__mockforge/verification/verify", post(verification::verify))
.route("/__mockforge/verification/count", post(verification::count))
.route("/__mockforge/verification/sequence", post(verification::verify_sequence_handler))
.route("/__mockforge/verification/never", post(verification::verify_never_handler))
.route("/__mockforge/verification/at-least", post(verification::verify_at_least_handler))
.route("/__mockforge/reality/level", get(get_reality_level))
.route("/__mockforge/reality/level", axum::routing::put(set_reality_level))
.route("/__mockforge/reality/presets", get(list_reality_presets))
.route("/__mockforge/reality/presets/import", post(import_reality_preset))
.route("/__mockforge/reality/presets/export", post(export_reality_preset))
.route("/__mockforge/continuum/ratio", get(get_continuum_ratio))
.route("/__mockforge/continuum/ratio", axum::routing::put(set_continuum_ratio))
.route("/__mockforge/continuum/schedule", get(get_continuum_schedule))
.route("/__mockforge/continuum/schedule", axum::routing::put(set_continuum_schedule))
.route("/__mockforge/continuum/advance", post(advance_continuum_ratio))
.route("/__mockforge/continuum/enabled", axum::routing::put(set_continuum_enabled))
.route("/__mockforge/continuum/overrides", get(get_continuum_overrides))
.route("/__mockforge/continuum/overrides", delete(clear_continuum_overrides))
.route("/__mockforge/contract-diff/upload", post(contract_diff::upload_request))
.route("/__mockforge/contract-diff/submit", post(contract_diff::submit_request))
.route("/__mockforge/contract-diff/captures", get(contract_diff::get_captured_requests))
.route("/__mockforge/contract-diff/captures/{id}", get(contract_diff::get_captured_request))
.route("/__mockforge/contract-diff/captures/{id}/analyze", post(contract_diff::analyze_captured_request))
.route("/__mockforge/contract-diff/captures/{id}/patch", post(contract_diff::generate_patch_file))
.route("/__mockforge/contract-diff/statistics", get(contract_diff::get_capture_statistics))
.route("/__mockforge/playground/endpoints", get(playground::list_playground_endpoints))
.route("/__mockforge/playground/execute", post(playground::execute_rest_request))
.route("/__mockforge/playground/graphql", post(playground::execute_graphql_query))
.route("/__mockforge/playground/graphql/introspect", get(playground::graphql_introspect))
.route("/__mockforge/playground/history", get(playground::get_request_history))
.route("/__mockforge/playground/history/{id}/replay", post(playground::replay_request))
.route("/__mockforge/playground/snippets", post(playground::generate_code_snippet))
.route("/api/v2/voice/process", post(voice::process_voice_command))
.route("/__mockforge/voice/process", post(voice::process_voice_command))
.route("/api/v2/voice/transpile-hook", post(voice::transpile_hook))
.route("/__mockforge/voice/transpile-hook", post(voice::transpile_hook))
.route(
"/api/v2/voice/create-workspace-scenario",
post(voice::create_workspace_scenario),
)
.route(
"/__mockforge/voice/create-workspace-scenario",
post(voice::create_workspace_scenario),
)
.route(
"/api/v2/voice/create-workspace-preview",
post(voice::create_workspace_preview),
)
.route(
"/__mockforge/voice/create-workspace-preview",
post(voice::create_workspace_preview),
)
.route("/api/v1/ai-studio/chat", post(ai_studio::chat))
.route("/__mockforge/ai-studio/chat", post(ai_studio::chat))
.route("/api/v1/ai-studio/generate-mock", post(ai_studio::generate_mock))
.route("/__mockforge/ai-studio/generate-mock", post(ai_studio::generate_mock))
.route("/api/v1/ai-studio/debug-test", post(ai_studio::debug_test))
.route("/__mockforge/ai-studio/debug-test", post(ai_studio::debug_test))
.route("/api/v1/ai-studio/debug/analyze-with-context", post(ai_studio::debug_analyze_with_context))
.route("/__mockforge/ai-studio/debug/analyze-with-context", post(ai_studio::debug_analyze_with_context))
.route("/api/v1/ai-studio/generate-persona", post(ai_studio::generate_persona))
.route("/__mockforge/ai-studio/generate-persona", post(ai_studio::generate_persona))
.route("/api/v1/ai-studio/freeze", post(ai_studio::freeze_artifact))
.route("/__mockforge/ai-studio/freeze", post(ai_studio::freeze_artifact))
.route("/api/v1/ai-studio/frozen", get(ai_studio::list_frozen))
.route("/__mockforge/ai-studio/frozen", get(ai_studio::list_frozen))
.route("/api/v1/ai-studio/apply-patch", post(ai_studio::apply_patch))
.route("/__mockforge/ai-studio/apply-patch", post(ai_studio::apply_patch))
.route("/api/v1/ai-studio/usage", get(ai_studio::get_usage))
.route("/__mockforge/ai-studio/usage", get(ai_studio::get_usage))
.route("/api/v1/ai-studio/org-controls", get(ai_studio::get_org_controls))
.route("/__mockforge/ai-studio/org-controls", get(ai_studio::get_org_controls))
.route("/api/v1/ai-studio/org-controls", axum::routing::put(ai_studio::update_org_controls))
.route("/__mockforge/ai-studio/org-controls", axum::routing::put(ai_studio::update_org_controls))
.route("/api/v1/ai-studio/org-controls/usage", get(ai_studio::get_org_usage))
.route("/__mockforge/ai-studio/org-controls/usage", get(ai_studio::get_org_usage))
.route("/api/v1/ai-studio/contract-diff/query", post(ai_studio::contract_diff_query))
.route("/__mockforge/ai-studio/contract-diff/query", post(ai_studio::contract_diff_query))
.route("/api/v2/failures/analyze", post(failure_analysis::analyze_failure))
.route("/api/v2/failures/{request_id}", get(failure_analysis::get_failure_analysis))
.route("/api/v2/failures/recent", get(failure_analysis::list_recent_failures))
.route("/__mockforge/failures/analyze", post(failure_analysis::analyze_failure))
.route("/__mockforge/failures/{request_id}", get(failure_analysis::get_failure_analysis))
.route("/__mockforge/failures/recent", get(failure_analysis::list_recent_failures))
.route("/__mockforge/community/showcase/projects", get(community::get_showcase_projects))
.route("/__mockforge/community/showcase/projects/{id}", get(community::get_showcase_project))
.route("/__mockforge/community/showcase/categories", get(community::get_showcase_categories))
.route("/__mockforge/community/showcase/stories", get(community::get_success_stories))
.route("/__mockforge/community/showcase/submit", post(community::submit_showcase_project))
.route("/__mockforge/community/learning/resources", get(community::get_learning_resources))
.route("/__mockforge/community/learning/resources/{id}", get(community::get_learning_resource))
.route("/__mockforge/community/learning/categories", get(community::get_learning_categories))
.route("/__mockforge/flows", get(behavioral_cloning::get_flows))
.route("/__mockforge/flows/{id}", get(behavioral_cloning::get_flow))
.route("/__mockforge/flows/{id}/tag", axum::routing::put(behavioral_cloning::tag_flow))
.route("/__mockforge/flows/{id}/compile", post(behavioral_cloning::compile_flow))
.route("/__mockforge/scenarios", get(behavioral_cloning::get_scenarios))
.route("/__mockforge/scenarios/{id}", get(behavioral_cloning::get_scenario))
.route("/__mockforge/scenarios/{id}/export", get(behavioral_cloning::export_scenario))
.route("/health/live", get(health::liveness_probe))
.route("/health/ready", get(health::readiness_probe))
.route("/health/startup", get(health::startup_probe))
.route("/health", get(health::deep_health_check))
.route("/healthz", get(health::deep_health_check))
.route("/readyz", get(health::readiness_probe))
.route("/livez", get(health::liveness_probe))
.route("/startupz", get(health::startup_probe))
.route("/__mockforge/chaos", get(chaos_api::get_chaos_status))
.route("/__mockforge/chaos/toggle", post(chaos_api::toggle_chaos))
.route("/__mockforge/chaos/scenarios/predefined", get(chaos_api::get_chaos_scenarios_predefined))
.route("/__mockforge/chaos/scenarios/{name}", post(chaos_api::start_chaos_scenario))
.route("/__mockforge/chaos/scenarios/{name}", delete(chaos_api::stop_chaos_scenario))
.route("/__mockforge/recorder/status", get(recorder_api::get_recorder_status))
.route("/__mockforge/recorder/start", post(recorder_api::start_recorder))
.route("/__mockforge/recorder/stop", post(recorder_api::stop_recorder))
.route("/__mockforge/world-state", get(world_state_proxy::get_world_state))
.route("/__mockforge/federation/peers", get(federation_api::get_federation_peers))
.route("/__mockforge/vbr/status", get(vbr_api::get_vbr_status));
let analytics_state = AnalyticsState::new(prometheus_url);
let analytics_router = Router::new()
.route("/__mockforge/analytics/summary", get(analytics::get_summary))
.route("/__mockforge/analytics/requests", get(analytics::get_requests))
.route("/__mockforge/analytics/endpoints", get(analytics::get_endpoints))
.route("/__mockforge/analytics/websocket", get(analytics::get_websocket))
.route("/__mockforge/analytics/smtp", get(analytics::get_smtp))
.route("/__mockforge/analytics/system", get(analytics::get_system))
.with_state(analytics_state);
router = router.merge(analytics_router);
{
use crate::handlers::protocol_contracts::{self, ProtocolContractsState};
let contracts_state = ProtocolContractsState::new();
let contracts_router = Router::new()
.route("/api/v1/contracts", get(protocol_contracts::list_contracts))
.route("/api/v1/contracts/grpc", post(protocol_contracts::create_grpc_contract))
.route(
"/api/v1/contracts/websocket",
post(protocol_contracts::create_websocket_contract),
)
.route("/api/v1/contracts/mqtt", post(protocol_contracts::create_mqtt_contract))
.route("/api/v1/contracts/kafka", post(protocol_contracts::create_kafka_contract))
.route("/api/v1/contracts/compare", post(protocol_contracts::compare_contracts))
.route("/api/v1/contracts/{contract_id}", get(protocol_contracts::get_contract))
.route("/api/v1/contracts/{contract_id}", delete(protocol_contracts::delete_contract))
.route(
"/api/v1/contracts/{contract_id}/validate",
post(protocol_contracts::validate_message),
)
.with_state(contracts_state);
router = router.merge(contracts_router);
tracing::info!("Protocol Contracts API routes mounted at /api/v1/contracts");
}
{
use crate::handlers::coverage_metrics::CoverageMetricsState;
use mockforge_analytics::AnalyticsDatabase;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::OnceCell;
let db_path = std::env::var("MOCKFORGE_ANALYTICS_DB_PATH")
.ok()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("analytics.db"));
let db_path_clone = db_path.clone();
let coverage_db = Arc::new(OnceCell::new());
let coverage_db_clone = coverage_db.clone();
tokio::spawn(async move {
match AnalyticsDatabase::new(&db_path_clone).await {
Ok(analytics_db) => {
if let Err(e) = analytics_db.run_migrations().await {
tracing::warn!("Failed to run analytics database migrations: {}. Coverage metrics routes may not work correctly.", e);
} else {
let _ = coverage_db_clone.set(analytics_db);
tracing::info!("Analytics database initialized for coverage metrics");
}
}
Err(e) => {
tracing::debug!("Failed to initialize analytics database for coverage metrics: {}. Coverage metrics routes will be unavailable.", e);
}
}
});
let coverage_state = CoverageMetricsState { db: coverage_db };
use crate::handlers::coverage_metrics;
router = router
.route("/api/v2/analytics/scenarios/usage", get(coverage_metrics::get_scenario_usage))
.route("/api/v2/analytics/personas/ci-hits", get(coverage_metrics::get_persona_ci_hits))
.route(
"/api/v2/analytics/endpoints/coverage",
get(coverage_metrics::get_endpoint_coverage),
)
.route(
"/api/v2/analytics/reality-levels/staleness",
get(coverage_metrics::get_reality_level_staleness),
)
.route(
"/api/v2/analytics/drift/percentage",
get(coverage_metrics::get_drift_percentage),
)
.layer(axum::extract::Extension(coverage_state));
tracing::info!(
"Coverage metrics routes mounted at /api/v2/analytics (database initializing)"
);
}
{
use crate::handlers::workspaces::WorkspaceState;
use mockforge_core::multi_tenant::{MultiTenantConfig, MultiTenantWorkspaceRegistry};
use std::sync::Arc;
let mt_config = MultiTenantConfig {
enabled: true,
default_workspace: "default".to_string(),
..Default::default()
};
let registry = MultiTenantWorkspaceRegistry::new(mt_config);
let workspace_state = WorkspaceState::new(Arc::new(tokio::sync::RwLock::new(registry)));
use crate::handlers::workspaces;
let workspace_router = Router::new()
.route("/__mockforge/workspaces", get(workspaces::list_workspaces))
.route("/__mockforge/workspaces", post(workspaces::create_workspace))
.route(
"/__mockforge/workspaces/order",
axum::routing::put(workspaces::update_workspaces_order),
)
.route("/__mockforge/workspaces/{workspace_id}", get(workspaces::get_workspace))
.route(
"/__mockforge/workspaces/{workspace_id}",
axum::routing::put(workspaces::update_workspace),
)
.route("/__mockforge/workspaces/{workspace_id}", delete(workspaces::delete_workspace))
.route("/__mockforge/workspaces/{workspace_id}/stats", get(workspaces::get_workspace_stats))
.route("/__mockforge/workspaces/{workspace_id}/environments", get(workspaces::list_environments))
.route("/__mockforge/workspaces/{workspace_id}/environments", post(workspaces::create_environment))
.route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}", axum::routing::put(workspaces::update_environment))
.route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}", delete(workspaces::delete_environment))
.route(
"/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/activate",
post(workspaces::set_active_environment),
)
.route(
"/__mockforge/workspaces/{workspace_id}/environments/order",
axum::routing::put(workspaces::update_environments_order),
)
.route(
"/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/variables",
get(workspaces::get_environment_variables),
)
.route(
"/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/variables",
post(workspaces::set_environment_variable),
)
.route(
"/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/variables/{variable_name}",
delete(workspaces::remove_environment_variable),
)
.route(
"/__mockforge/workspaces/{workspace_id}/autocomplete",
post(workspaces::get_autocomplete_suggestions),
)
.route(
"/__mockforge/workspaces/{workspace_id}/sync/status",
get(workspaces::get_sync_status),
)
.route(
"/__mockforge/workspaces/{workspace_id}/sync/configure",
post(workspaces::configure_sync),
)
.route(
"/__mockforge/workspaces/{workspace_id}/sync/disable",
post(workspaces::disable_sync),
)
.route(
"/__mockforge/workspaces/{workspace_id}/sync/trigger",
post(workspaces::trigger_sync),
)
.route(
"/__mockforge/workspaces/{workspace_id}/sync/changes",
get(workspaces::get_sync_changes),
)
.route(
"/__mockforge/workspaces/{workspace_id}/sync/confirm",
post(workspaces::confirm_sync_changes),
)
.route("/__mockforge/workspaces/{workspace_id}/mock-environments", get(workspaces::list_mock_environments))
.route("/__mockforge/workspaces/{workspace_id}/mock-environments/{env_name}", get(workspaces::get_mock_environment))
.route("/__mockforge/workspaces/{workspace_id}/mock-environments/{env_name}", axum::routing::put(workspaces::update_mock_environment))
.route("/__mockforge/workspaces/{workspace_id}/mock-environments/active", post(workspaces::set_active_mock_environment))
.route(
"/__mockforge/workspaces/{workspace_id}/activate",
post(workspaces::set_active_workspace),
)
.route("/api/v2/voice/create-workspace-confirm", post(voice::create_workspace_confirm))
.route(
"/__mockforge/voice/create-workspace-confirm",
post(voice::create_workspace_confirm),
)
.with_state(workspace_state.clone());
router = router.merge(workspace_router);
tracing::info!("Workspace router mounted with WorkspaceState");
#[cfg(feature = "database-auth")]
{
use crate::handlers::promotions;
use crate::handlers::promotions::PromotionState;
use mockforge_collab::promotion::PromotionService;
use sqlx::SqlitePool;
use std::sync::Arc;
let db_url = std::env::var("MOCKFORGE_COLLAB_DB_URL")
.unwrap_or_else(|_| "sqlite://mockforge-collab.db".to_string());
match SqlitePool::connect_lazy(&db_url) {
Ok(pool) => {
let promotion_service = Arc::new(PromotionService::new(pool));
let promotion_state = PromotionState::new(promotion_service, workspace_state);
let promotion_router = Router::new()
.route("/api/v2/promotions", post(promotions::create_promotion))
.route("/api/v2/promotions/{promotion_id}", get(promotions::get_promotion))
.route(
"/api/v2/promotions/{promotion_id}/status",
axum::routing::put(promotions::update_promotion_status),
)
.route(
"/api/v2/workspaces/{workspace_id}/promotions",
get(promotions::list_workspace_promotions),
)
.route(
"/api/v2/promotions/pending",
get(promotions::list_pending_promotions),
)
.route(
"/api/v2/workspaces/{workspace_id}/promotions/{entity_type}/{entity_id}",
get(promotions::get_entity_promotion_history),
)
.route("/__mockforge/promotions", post(promotions::create_promotion))
.route(
"/__mockforge/promotions/{promotion_id}",
get(promotions::get_promotion),
)
.route(
"/__mockforge/promotions/{promotion_id}/status",
axum::routing::put(promotions::update_promotion_status),
)
.route(
"/__mockforge/workspaces/{workspace_id}/promotions",
get(promotions::list_workspace_promotions),
)
.route(
"/__mockforge/promotions/pending",
get(promotions::list_pending_promotions),
)
.route(
"/__mockforge/workspaces/{workspace_id}/promotions/{entity_type}/{entity_id}",
get(promotions::get_entity_promotion_history),
)
.with_state(promotion_state);
router = router.merge(promotion_router);
tracing::info!("Promotion routes mounted");
}
Err(e) => {
tracing::warn!(
"Failed to initialize promotion database pool from {}: {}",
db_url,
e
);
}
}
}
#[cfg(not(feature = "database-auth"))]
{
tracing::debug!("Promotion routes require 'database-auth' feature - not available");
}
}
{
use mockforge_core::config::ServerConfig;
use mockforge_http::{create_ui_builder_router, UIBuilderState};
let server_config = ServerConfig::default();
let ui_builder_state = UIBuilderState::new(server_config);
let ui_builder_router = create_ui_builder_router(ui_builder_state);
router = router.nest_service("/__mockforge/ui-builder", ui_builder_router);
tracing::info!("UI Builder mounted at /__mockforge/ui-builder");
}
{
use mockforge_http::handlers::conformance::{conformance_router, ConformanceState};
let conformance_state = ConformanceState::new();
router = router.nest_service("/api/conformance", conformance_router(conformance_state));
tracing::info!("Conformance testing API routes mounted at /api/conformance");
}
router = router.route("/{*path}", get(serve_admin_html));
router = router.layer(from_fn(rbac_middleware));
router.layer(CompressionLayer::new()).layer(cors).with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_admin_router() {
let http_addr: std::net::SocketAddr = "127.0.0.1:3000".parse().unwrap();
let router = create_admin_router(
Some(http_addr),
None,
None,
None,
true,
8080,
"http://localhost:9090".to_string(),
None,
None,
None,
None,
None,
None,
None,
None,
);
let _ = router;
}
#[tokio::test]
async fn test_create_admin_router_no_servers() {
let router = create_admin_router(
None,
None,
None,
None,
false,
8080,
"http://localhost:9090".to_string(),
None,
None,
None,
None,
None,
None,
None,
None,
);
let _ = router;
}
fn make_test_router() -> Router {
create_admin_router(
None,
None,
None,
None,
true,
8080,
"http://localhost:9090".to_string(),
None,
None,
None,
None,
None,
None,
None,
None,
)
}
async fn send(
router: Router,
method: &str,
uri: &str,
body: Option<&str>,
auth_token: Option<&str>,
) -> (axum::http::StatusCode, String, String) {
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
let mut builder = Request::builder().method(method).uri(uri);
if let Some(token) = auth_token {
builder = builder.header("authorization", format!("Bearer {}", token));
}
if body.is_some() {
builder = builder.header("content-type", "application/json");
}
let req = builder.body(Body::from(body.unwrap_or("").to_string())).unwrap();
let response = router.oneshot(req).await.unwrap();
let status = response.status();
let content_type = response
.headers()
.get("content-type")
.map(|v| v.to_str().unwrap().to_string())
.unwrap_or_default();
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
let text = String::from_utf8_lossy(&bytes).to_string();
(status, content_type, text)
}
async fn send_authed(
router: Router,
method: &str,
uri: &str,
body: Option<&str>,
) -> (axum::http::StatusCode, String, String) {
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
let mut builder = Request::builder()
.method(method)
.uri(uri)
.header("x-user-id", "admin-001")
.header("x-username", "admin")
.header("x-user-role", "admin");
if body.is_some() {
builder = builder.header("content-type", "application/json");
}
let req = builder.body(Body::from(body.unwrap_or("").to_string())).unwrap();
let response = router.oneshot(req).await.unwrap();
let status = response.status();
let content_type = response
.headers()
.get("content-type")
.map(|v| v.to_str().unwrap().to_string())
.unwrap_or_default();
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
let text = String::from_utf8_lossy(&bytes).to_string();
(status, content_type, text)
}
#[tokio::test]
async fn test_tui_get_endpoints_return_json_without_auth() {
let router = make_test_router();
let endpoints = vec![
"/__mockforge/chaos",
"/__mockforge/recorder/status",
"/__mockforge/world-state",
"/__mockforge/federation/peers",
"/__mockforge/vbr/status",
"/__mockforge/chaos/scenarios/predefined",
];
for endpoint in endpoints {
let (status, content_type, body) =
send(router.clone(), "GET", endpoint, None, None).await;
assert_eq!(
status,
axum::http::StatusCode::OK,
"GET {} returned status {} — body: {}",
endpoint,
status,
&body[..body.len().min(200)]
);
assert!(
content_type.contains("application/json"),
"GET {} returned content-type '{}' instead of JSON — body: {}",
endpoint,
content_type,
&body[..body.len().min(200)]
);
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap_or_else(|e| {
panic!(
"GET {} returned non-JSON body: {} — error: {}",
endpoint,
&body[..body.len().min(200)],
e
)
});
assert_eq!(
parsed["success"],
serde_json::Value::Bool(true),
"GET {} missing success:true — got: {}",
endpoint,
parsed
);
}
}
#[tokio::test]
async fn test_tui_post_endpoints_require_auth() {
let router = make_test_router();
let post_endpoints = vec![
("/__mockforge/chaos/toggle", r#"{"enabled":true}"#),
("/__mockforge/recorder/start", ""),
("/__mockforge/recorder/stop", ""),
];
for (endpoint, body) in &post_endpoints {
let req_body = if body.is_empty() { None } else { Some(*body) };
let (status, _, _) = send(router.clone(), "POST", endpoint, req_body, None).await;
assert_eq!(
status,
axum::http::StatusCode::UNAUTHORIZED,
"POST {} should require auth but returned {}",
endpoint,
status
);
}
}
#[tokio::test]
async fn test_tui_post_endpoints_with_auth() {
let router = make_test_router();
let (status, ct, body) = send_authed(
router.clone(),
"POST",
"/__mockforge/chaos/toggle",
Some(r#"{"enabled":true}"#),
)
.await;
assert_eq!(status, axum::http::StatusCode::OK, "POST chaos/toggle failed: {}", body);
assert!(ct.contains("application/json"), "chaos/toggle not JSON");
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(parsed.get("success").is_some(), "chaos/toggle missing success field");
let (status, ct, body) =
send_authed(router.clone(), "POST", "/__mockforge/recorder/start", None).await;
assert_eq!(status, axum::http::StatusCode::OK, "POST recorder/start failed: {}", body);
assert!(ct.contains("application/json"), "recorder/start not JSON");
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(parsed.get("success").is_some(), "recorder/start missing success field");
let (status, ct, body) =
send_authed(router.clone(), "POST", "/__mockforge/recorder/stop", None).await;
assert_eq!(status, axum::http::StatusCode::OK, "POST recorder/stop failed: {}", body);
assert!(ct.contains("application/json"), "recorder/stop not JSON");
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(parsed.get("success").is_some(), "recorder/stop missing success field");
let (status, ct, body) =
send_authed(router.clone(), "POST", "/__mockforge/chaos/scenarios/test-scenario", None)
.await;
assert!(ct.contains("application/json"), "start scenario not JSON");
assert!(
status == axum::http::StatusCode::OK
|| status == axum::http::StatusCode::SERVICE_UNAVAILABLE,
"POST chaos/scenarios/test-scenario returned unexpected status {} — body: {}",
status,
body
);
let (status, ct, body) = send_authed(
router.clone(),
"DELETE",
"/__mockforge/chaos/scenarios/test-scenario",
None,
)
.await;
assert!(ct.contains("application/json"), "stop scenario not JSON");
assert!(
status == axum::http::StatusCode::OK
|| status == axum::http::StatusCode::SERVICE_UNAVAILABLE,
"DELETE chaos/scenarios/test-scenario returned unexpected status {} — body: {}",
status,
body
);
}
#[tokio::test]
async fn test_tui_get_endpoints_response_structure() {
let router = make_test_router();
let (_, _, body) = send(router.clone(), "GET", "/__mockforge/chaos", None, None).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let data = &v["data"];
assert!(data.get("enabled").is_some(), "chaos missing 'enabled'");
assert!(
data.get("active_scenario_count").is_some(),
"chaos missing 'active_scenario_count'"
);
let (_, _, body) =
send(router.clone(), "GET", "/__mockforge/recorder/status", None, None).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let data = &v["data"];
assert!(data.get("recording").is_some(), "recorder missing 'recording'");
assert!(data.get("recorded_count").is_some(), "recorder missing 'recorded_count'");
let (_, _, body) =
send(router.clone(), "GET", "/__mockforge/federation/peers", None, None).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(v["data"].is_array(), "federation peers should be an array");
let (_, _, body) = send(router.clone(), "GET", "/__mockforge/vbr/status", None, None).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let data = &v["data"];
assert!(data.get("enabled").is_some(), "vbr missing 'enabled'");
assert!(data.get("model_count").is_some(), "vbr missing 'model_count'");
assert!(data.get("training_status").is_some(), "vbr missing 'training_status'");
let (_, _, body) =
send(router.clone(), "GET", "/__mockforge/chaos/scenarios/predefined", None, None)
.await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(v["success"].as_bool().unwrap(), "predefined scenarios not success");
let scenarios = v["data"].as_array().expect("predefined scenarios should be array");
if !scenarios.is_empty() {
assert!(scenarios[0].get("name").is_some(), "scenario missing 'name'");
assert!(scenarios[0].get("description").is_some(), "scenario missing 'description'");
assert!(scenarios[0].get("severity").is_some(), "scenario missing 'severity'");
}
}
}