mod auth;
mod contracts;
mod operational;
mod protocol;
mod routes;
pub use auth::*;
pub use contracts::*;
pub use operational::*;
pub use protocol::*;
pub use routes::*;
use crate::{Config as CoreConfig, Error, RealityLevel, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use tokio::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealitySliderConfig {
pub level: RealityLevel,
pub enabled: bool,
}
impl Default for RealitySliderConfig {
fn default() -> Self {
Self {
level: RealityLevel::ModerateRealism,
enabled: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(default)]
pub struct ServerConfig {
pub http: HttpConfig,
pub websocket: WebSocketConfig,
pub graphql: GraphQLConfig,
pub grpc: GrpcConfig,
pub mqtt: MqttConfig,
pub smtp: SmtpConfig,
pub ftp: FtpConfig,
pub kafka: KafkaConfig,
pub amqp: AmqpConfig,
pub tcp: TcpConfig,
pub admin: AdminConfig,
pub chaining: ChainingConfig,
pub core: CoreConfig,
pub logging: LoggingConfig,
pub data: DataConfig,
#[serde(default)]
pub mockai: MockAIConfig,
pub observability: ObservabilityConfig,
pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
#[serde(default)]
pub routes: Vec<RouteConfig>,
#[serde(default)]
pub protocols: ProtocolsConfig,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub profiles: HashMap<String, ProfileConfig>,
#[serde(default)]
pub deceptive_deploy: DeceptiveDeployConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub behavioral_cloning: Option<BehavioralCloningConfig>,
#[serde(default)]
pub reality: RealitySliderConfig,
#[serde(default)]
pub reality_continuum: crate::reality_continuum::ContinuumConfig,
#[serde(default)]
pub security: SecurityConfig,
#[serde(default)]
pub drift_budget: crate::contract_drift::DriftBudgetConfig,
#[serde(default)]
pub incidents: IncidentConfig,
#[serde(default)]
pub pr_generation: crate::pr_generation::PRGenerationConfig,
#[serde(default)]
pub consumer_contracts: ConsumerContractsConfig,
#[serde(default)]
pub contracts: ContractsConfig,
#[serde(default)]
pub behavioral_economics: BehavioralEconomicsConfig,
#[serde(default)]
pub drift_learning: DriftLearningConfig,
#[serde(default)]
pub org_ai_controls: crate::ai_studio::org_controls::OrgAiControlsConfig,
#[serde(default)]
pub performance: PerformanceConfig,
#[serde(default)]
pub plugins: PluginResourceConfig,
#[serde(default)]
pub hot_reload: ConfigHotReloadConfig,
#[serde(default)]
pub secrets: SecretBackendConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(default)]
pub struct ProfileConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub http: Option<HttpConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub websocket: Option<WebSocketConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub graphql: Option<GraphQLConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grpc: Option<GrpcConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mqtt: Option<MqttConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub smtp: Option<SmtpConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ftp: Option<FtpConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kafka: Option<KafkaConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amqp: Option<AmqpConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tcp: Option<TcpConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin: Option<AdminConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chaining: Option<ChainingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub core: Option<CoreConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<LoggingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<DataConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mockai: Option<MockAIConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub observability: Option<ObservabilityConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub routes: Option<Vec<RouteConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocols: Option<ProtocolsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deceptive_deploy: Option<DeceptiveDeployConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reality: Option<RealitySliderConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security: Option<SecurityConfig>,
}
impl ServerConfig {
pub fn minimal() -> Self {
Self::default()
}
pub fn development() -> Self {
let mut cfg = Self::default();
cfg.admin.enabled = true;
cfg.logging.level = "debug".to_string();
cfg
}
pub fn ci() -> Self {
let mut cfg = Self::default();
cfg.core.latency_enabled = false;
cfg.core.failures_enabled = false;
cfg
}
#[must_use]
pub fn with_http_port(mut self, port: u16) -> Self {
self.http.port = port;
self
}
#[must_use]
pub fn with_admin(mut self, port: u16) -> Self {
self.admin.enabled = true;
self.admin.port = port;
self
}
#[must_use]
pub fn with_grpc(mut self, port: u16) -> Self {
self.grpc.enabled = true;
self.grpc.port = port;
self.protocols.grpc.enabled = true;
self
}
#[must_use]
pub fn with_websocket(mut self, port: u16) -> Self {
self.websocket.enabled = true;
self.websocket.port = port;
self.protocols.websocket.enabled = true;
self
}
#[must_use]
pub fn with_log_level(mut self, level: &str) -> Self {
self.logging.level = level.to_string();
self
}
pub fn has_advanced_features(&self) -> bool {
self.mockai.enabled
|| self.behavioral_cloning.as_ref().is_some_and(|bc| bc.enabled)
|| self.reality_continuum.enabled
}
pub fn has_enterprise_features(&self) -> bool {
self.multi_tenant.enabled || self.security.monitoring.siem.enabled
}
}
pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
let content = fs::read_to_string(&path)
.await
.map_err(|e| Error::io_with_context("reading config file", e.to_string()))?;
let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
|| path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
{
serde_yaml::from_str(&content).map_err(|e| {
let error_msg = e.to_string();
let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
if error_msg.contains("missing field") {
full_msg.push_str(
"\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
);
full_msg.push_str(
"\n Omit fields you don't need - MockForge will use sensible defaults.",
);
full_msg.push_str("\n See config.template.yaml for all available options.");
} else if error_msg.contains("unknown field") {
full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
full_msg.push_str("\n See config.template.yaml for valid field names.");
}
Error::config(full_msg)
})?
} else {
serde_json::from_str(&content).map_err(|e| {
let error_msg = e.to_string();
let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
if error_msg.contains("missing field") {
full_msg.push_str(
"\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
);
full_msg.push_str(
"\n Omit fields you don't need - MockForge will use sensible defaults.",
);
full_msg.push_str("\n See config.template.yaml for all available options.");
} else if error_msg.contains("unknown field") {
full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
full_msg.push_str("\n See config.template.yaml for valid field names.");
}
Error::config(full_msg)
})?
};
Ok(config)
}
pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
|| path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
{
serde_yaml::to_string(config)
.map_err(|e| Error::config(format!("Failed to serialize config to YAML: {}", e)))?
} else {
serde_json::to_string_pretty(config)
.map_err(|e| Error::config(format!("Failed to serialize config to JSON: {}", e)))?
};
fs::write(path, content)
.await
.map_err(|e| Error::io_with_context("writing config file", e.to_string()))?;
Ok(())
}
pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
match load_config(&path).await {
Ok(config) => {
tracing::info!("Loaded configuration from {:?}", path.as_ref());
config
}
Err(e) => {
tracing::warn!(
"Failed to load config from {:?}: {}. Using defaults.",
path.as_ref(),
e
);
ServerConfig::default()
}
}
}
pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
let config = ServerConfig::default();
save_config(path, &config).await?;
Ok(())
}
pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
if let Ok(port_num) = port.parse() {
config.http.port = port_num;
}
}
if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
config.http.host = host;
}
if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
if let Ok(port_num) = port.parse() {
config.websocket.port = port_num;
}
}
if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
if let Ok(port_num) = port.parse() {
config.grpc.port = port_num;
}
}
if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
if let Ok(port_num) = port.parse() {
config.smtp.port = port_num;
}
}
if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
config.smtp.host = host;
}
if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
}
if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
config.smtp.hostname = hostname;
}
if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
if let Ok(port_num) = port.parse() {
config.tcp.port = port_num;
}
}
if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
config.tcp.host = host;
}
if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
}
if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
if let Ok(port_num) = port.parse() {
config.admin.port = port_num;
}
}
if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
config.admin.enabled = true;
}
if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
config.admin.host = host;
}
if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
if !mount_path.trim().is_empty() {
config.admin.mount_path = Some(mount_path);
}
}
if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
config.admin.api_enabled = on;
}
if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
config.admin.prometheus_url = prometheus_url;
}
if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
config.core.latency_enabled = enabled;
}
if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
config.core.failures_enabled = enabled;
}
if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
config.core.overrides_enabled = enabled;
}
if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
let enabled =
traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
config.core.traffic_shaping_enabled = enabled;
}
if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
config.core.traffic_shaping.bandwidth.enabled = enabled;
}
if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
if let Ok(bytes) = max_bytes_per_sec.parse() {
config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
config.core.traffic_shaping.bandwidth.enabled = true;
}
}
if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
if let Ok(bytes) = burst_capacity.parse() {
config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
}
}
if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
config.core.traffic_shaping.burst_loss.enabled = enabled;
}
if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
if let Ok(prob) = burst_probability.parse::<f64>() {
config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
config.core.traffic_shaping.burst_loss.enabled = true;
}
}
if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
if let Ok(ms) = burst_duration.parse() {
config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
}
}
if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
if let Ok(rate) = loss_rate.parse::<f64>() {
config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
}
}
if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
if let Ok(ms) = recovery_time.parse() {
config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
}
}
if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
config.logging.level = level;
}
config
}
pub fn validate_config(config: &ServerConfig) -> Result<()> {
if config.http.port == 0 {
return Err(Error::config("HTTP port cannot be 0"));
}
if config.websocket.port == 0 {
return Err(Error::config("WebSocket port cannot be 0"));
}
if config.grpc.port == 0 {
return Err(Error::config("gRPC port cannot be 0"));
}
if config.admin.port == 0 {
return Err(Error::config("Admin port cannot be 0"));
}
let ports = [
("HTTP", config.http.port),
("WebSocket", config.websocket.port),
("gRPC", config.grpc.port),
("Admin", config.admin.port),
];
for i in 0..ports.len() {
for j in (i + 1)..ports.len() {
if ports[i].1 == ports[j].1 {
return Err(Error::config(format!(
"Port conflict: {} and {} both use port {}",
ports[i].0, ports[j].0, ports[i].1
)));
}
}
}
let valid_levels = ["trace", "debug", "info", "warn", "error"];
if !valid_levels.contains(&config.logging.level.as_str()) {
return Err(Error::config(format!(
"Invalid log level: {}. Valid levels: {}",
config.logging.level,
valid_levels.join(", ")
)));
}
Ok(())
}
pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
macro_rules! merge_field {
($field:ident) => {
if let Some(override_val) = profile.$field {
base.$field = override_val;
}
};
}
merge_field!(http);
merge_field!(websocket);
merge_field!(graphql);
merge_field!(grpc);
merge_field!(mqtt);
merge_field!(smtp);
merge_field!(ftp);
merge_field!(kafka);
merge_field!(amqp);
merge_field!(tcp);
merge_field!(admin);
merge_field!(chaining);
merge_field!(core);
merge_field!(logging);
merge_field!(data);
merge_field!(mockai);
merge_field!(observability);
merge_field!(multi_tenant);
merge_field!(routes);
merge_field!(protocols);
base
}
pub async fn load_config_with_profile<P: AsRef<Path>>(
path: P,
profile_name: Option<&str>,
) -> Result<ServerConfig> {
let mut config = load_config_auto(&path).await?;
if let Some(profile) = profile_name {
if let Some(profile_config) = config.profiles.remove(profile) {
tracing::info!("Applying profile: {}", profile);
config = apply_profile(config, profile_config);
} else {
return Err(Error::config(format!(
"Profile '{}' not found in configuration. Available profiles: {}",
profile,
config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
)));
}
}
config.profiles.clear();
Ok(config)
}
#[cfg(feature = "scripting")]
pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
use rquickjs::{Context, Runtime};
let content = fs::read_to_string(&path)
.await
.map_err(|e| Error::io_with_context("reading JS/TS config file", e.to_string()))?;
let runtime =
Runtime::new().map_err(|e| Error::config(format!("Failed to create JS runtime: {}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| Error::config(format!("Failed to create JS context: {}", e)))?;
context.with(|ctx| {
let js_content = if path
.as_ref()
.extension()
.and_then(|s| s.to_str())
.map(|ext| ext == "ts")
.unwrap_or(false)
{
strip_typescript_types(&content)?
} else {
content
};
let result: rquickjs::Value = ctx
.eval(js_content.as_bytes())
.map_err(|e| Error::config(format!("Failed to evaluate JS config: {}", e)))?;
let json_str: String = ctx
.json_stringify(result)
.map_err(|e| Error::config(format!("Failed to stringify JS config: {}", e)))?
.ok_or_else(|| Error::config("JS config returned undefined"))?
.get()
.map_err(|e| Error::config(format!("Failed to get JSON string: {}", e)))?;
serde_json::from_str(&json_str)
.map_err(|e| Error::config(format!("Failed to parse JS config as ServerConfig: {}", e)))
})
}
#[cfg(feature = "scripting")]
fn strip_typescript_types(content: &str) -> Result<String> {
use regex::Regex;
let mut result = content.to_string();
let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
.map_err(|e| Error::config(format!("Failed to compile interface regex: {}", e)))?;
result = interface_re.replace_all(&result, "").to_string();
let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
.map_err(|e| Error::config(format!("Failed to compile type alias regex: {}", e)))?;
result = type_alias_re.replace_all(&result, "").to_string();
let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
.map_err(|e| Error::config(format!("Failed to compile type annotation regex: {}", e)))?;
result = type_annotation_re.replace_all(&result, "").to_string();
let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
.map_err(|e| Error::config(format!("Failed to compile type import regex: {}", e)))?;
result = type_import_re.replace_all(&result, "").to_string();
let as_type_re = Regex::new(r"\s+as\s+\w+")
.map_err(|e| Error::config(format!("Failed to compile 'as type' regex: {}", e)))?;
result = as_type_re.replace_all(&result, "").to_string();
Ok(result)
}
pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
match ext {
#[cfg(feature = "scripting")]
"ts" | "js" => load_config_from_js(&path).await,
#[cfg(not(feature = "scripting"))]
"ts" | "js" => Err(Error::config(
"JS/TS config files require the 'scripting' feature (rquickjs). \
Enable it with: cargo build --features scripting"
.to_string(),
)),
"yaml" | "yml" | "json" => load_config(&path).await,
_ => Err(Error::config(format!(
"Unsupported config file format: {}. Supported: .yaml, .yml, .json{}",
ext,
if cfg!(feature = "scripting") {
", .ts, .js"
} else {
""
}
))),
}
}
pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
let current_dir = std::env::current_dir()
.map_err(|e| Error::config(format!("Failed to get current directory: {}", e)))?;
let config_names = vec![
"mockforge.config.ts",
"mockforge.config.js",
"mockforge.yaml",
"mockforge.yml",
".mockforge.yaml",
".mockforge.yml",
];
for name in &config_names {
let path = current_dir.join(name);
if fs::metadata(&path).await.is_ok() {
return Ok(path);
}
}
let mut dir = current_dir.clone();
for _ in 0..5 {
if let Some(parent) = dir.parent() {
for name in &config_names {
let path = parent.join(name);
if fs::metadata(&path).await.is_ok() {
return Ok(path);
}
}
dir = parent.to_path_buf();
} else {
break;
}
}
Err(Error::config(
"No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ServerConfig::default();
assert_eq!(config.http.port, 3000);
assert_eq!(config.websocket.port, 3001);
assert_eq!(config.grpc.port, 50051);
assert_eq!(config.admin.port, 9080);
}
#[test]
fn test_config_validation() {
let mut config = ServerConfig::default();
assert!(validate_config(&config).is_ok());
config.websocket.port = config.http.port;
assert!(validate_config(&config).is_err());
config.websocket.port = 3001; config.logging.level = "invalid".to_string();
assert!(validate_config(&config).is_err());
}
#[test]
fn test_apply_profile() {
let base = ServerConfig::default();
assert_eq!(base.http.port, 3000);
let profile = ProfileConfig {
http: Some(HttpConfig {
port: 8080,
..Default::default()
}),
logging: Some(LoggingConfig {
level: "debug".to_string(),
..Default::default()
}),
..Default::default()
};
let merged = apply_profile(base, profile);
assert_eq!(merged.http.port, 8080);
assert_eq!(merged.logging.level, "debug");
assert_eq!(merged.websocket.port, 3001); }
#[test]
fn test_minimal_config() {
let config = ServerConfig::minimal();
assert_eq!(config.http.port, 3000);
assert!(!config.admin.enabled);
}
#[test]
fn test_development_config() {
let config = ServerConfig::development();
assert!(config.admin.enabled);
assert_eq!(config.logging.level, "debug");
}
#[test]
fn test_ci_config() {
let config = ServerConfig::ci();
assert!(!config.core.latency_enabled);
assert!(!config.core.failures_enabled);
}
#[test]
fn test_builder_with_http_port() {
let config = ServerConfig::minimal().with_http_port(8080);
assert_eq!(config.http.port, 8080);
}
#[test]
fn test_builder_with_admin() {
let config = ServerConfig::minimal().with_admin(9090);
assert!(config.admin.enabled);
assert_eq!(config.admin.port, 9090);
}
#[test]
fn test_builder_with_grpc() {
let config = ServerConfig::minimal().with_grpc(50052);
assert!(config.grpc.enabled);
assert_eq!(config.grpc.port, 50052);
assert!(config.protocols.grpc.enabled);
}
#[test]
fn test_builder_with_websocket() {
let config = ServerConfig::minimal().with_websocket(3002);
assert!(config.websocket.enabled);
assert_eq!(config.websocket.port, 3002);
}
#[test]
fn test_builder_with_log_level() {
let config = ServerConfig::minimal().with_log_level("trace");
assert_eq!(config.logging.level, "trace");
}
#[test]
fn test_has_advanced_features_default() {
let config = ServerConfig::minimal();
assert!(!config.has_advanced_features());
}
#[test]
fn test_has_enterprise_features_default() {
let config = ServerConfig::minimal();
assert!(!config.has_enterprise_features());
}
#[test]
#[cfg(feature = "scripting")]
fn test_strip_typescript_types() {
let ts_code = r#"
interface Config {
port: number;
host: string;
}
const config: Config = {
port: 3000,
host: "localhost"
} as Config;
"#;
let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
assert!(!stripped.contains("interface"));
assert!(!stripped.contains(": Config"));
assert!(!stripped.contains("as Config"));
}
}