1mod auth;
4mod contracts;
5mod operational;
6mod protocol;
7mod routes;
8
9pub use auth::*;
10pub use contracts::*;
11pub use operational::*;
12pub use protocol::*;
13pub use routes::*;
14
15use crate::{Config as CoreConfig, Error, RealityLevel, Result};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::path::Path;
19use tokio::fs;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(default)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29pub struct RealitySliderConfig {
30 pub level: RealityLevel,
32 pub enabled: bool,
34}
35
36impl Default for RealitySliderConfig {
37 fn default() -> Self {
38 Self {
39 level: RealityLevel::ModerateRealism,
40 enabled: true,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[serde(default)]
49pub struct ServerConfig {
50 pub http: HttpConfig,
52 pub websocket: WebSocketConfig,
54 pub graphql: GraphQLConfig,
56 pub grpc: GrpcConfig,
58 pub mqtt: MqttConfig,
60 pub smtp: SmtpConfig,
62 pub ftp: FtpConfig,
64 pub kafka: KafkaConfig,
66 pub amqp: AmqpConfig,
68 pub tcp: TcpConfig,
70 pub admin: AdminConfig,
72 pub chaining: ChainingConfig,
74 pub core: CoreConfig,
76 pub logging: LoggingConfig,
78 pub data: DataConfig,
80 #[serde(default)]
82 pub mockai: MockAIConfig,
83 pub observability: ObservabilityConfig,
85 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
87 #[serde(default)]
89 pub routes: Vec<RouteConfig>,
90 #[serde(default)]
92 pub protocols: ProtocolsConfig,
93 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
95 pub profiles: HashMap<String, ProfileConfig>,
96 #[serde(default)]
98 pub deceptive_deploy: DeceptiveDeployConfig,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub behavioral_cloning: Option<BehavioralCloningConfig>,
102 #[serde(default)]
104 pub reality: RealitySliderConfig,
105 #[serde(default)]
107 pub reality_continuum: crate::reality_continuum::ContinuumConfig,
108 #[serde(default)]
110 pub security: SecurityConfig,
111 #[serde(default)]
113 pub drift_budget: crate::contract_drift::DriftBudgetConfig,
114 #[serde(default)]
116 pub incidents: IncidentConfig,
117 #[serde(default)]
119 pub pr_generation: crate::pr_generation::PRGenerationConfig,
120 #[serde(default)]
122 pub consumer_contracts: ConsumerContractsConfig,
123 #[serde(default)]
125 pub contracts: ContractsConfig,
126 #[serde(default)]
128 pub behavioral_economics: BehavioralEconomicsConfig,
129 #[serde(default)]
131 pub drift_learning: DriftLearningConfig,
132 #[serde(default)]
134 pub org_ai_controls: crate::ai_studio::org_controls::OrgAiControlsConfig,
135 #[serde(default)]
137 pub performance: PerformanceConfig,
138 #[serde(default)]
140 pub plugins: PluginResourceConfig,
141 #[serde(default)]
143 pub hot_reload: ConfigHotReloadConfig,
144 #[serde(default)]
146 pub secrets: SecretBackendConfig,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, Default)]
151#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[serde(default)]
153pub struct ProfileConfig {
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub http: Option<HttpConfig>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub websocket: Option<WebSocketConfig>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub graphql: Option<GraphQLConfig>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub grpc: Option<GrpcConfig>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub mqtt: Option<MqttConfig>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub smtp: Option<SmtpConfig>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub ftp: Option<FtpConfig>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub kafka: Option<KafkaConfig>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub amqp: Option<AmqpConfig>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub tcp: Option<TcpConfig>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub admin: Option<AdminConfig>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub chaining: Option<ChainingConfig>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub core: Option<CoreConfig>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub logging: Option<LoggingConfig>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub data: Option<DataConfig>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub mockai: Option<MockAIConfig>,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub observability: Option<ObservabilityConfig>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub routes: Option<Vec<RouteConfig>>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub protocols: Option<ProtocolsConfig>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub deceptive_deploy: Option<DeceptiveDeployConfig>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub reality: Option<RealitySliderConfig>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub security: Option<SecurityConfig>,
226}
227
228pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
232 let content = fs::read_to_string(&path)
233 .await
234 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
235
236 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
238 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
239 {
240 serde_yaml::from_str(&content).map_err(|e| {
241 let error_msg = e.to_string();
243 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
244
245 if error_msg.contains("missing field") {
247 full_msg.push_str(
248 "\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
249 );
250 full_msg.push_str(
251 "\n Omit fields you don't need - MockForge will use sensible defaults.",
252 );
253 full_msg.push_str("\n See config.template.yaml for all available options.");
254 } else if error_msg.contains("unknown field") {
255 full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
256 full_msg.push_str("\n See config.template.yaml for valid field names.");
257 }
258
259 Error::generic(full_msg)
260 })?
261 } else {
262 serde_json::from_str(&content).map_err(|e| {
263 let error_msg = e.to_string();
265 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
266
267 if error_msg.contains("missing field") {
269 full_msg.push_str(
270 "\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
271 );
272 full_msg.push_str(
273 "\n Omit fields you don't need - MockForge will use sensible defaults.",
274 );
275 full_msg.push_str("\n See config.template.yaml for all available options.");
276 } else if error_msg.contains("unknown field") {
277 full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
278 full_msg.push_str("\n See config.template.yaml for valid field names.");
279 }
280
281 Error::generic(full_msg)
282 })?
283 };
284
285 Ok(config)
286}
287
288pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
290 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
291 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
292 {
293 serde_yaml::to_string(config)
294 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
295 } else {
296 serde_json::to_string_pretty(config)
297 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
298 };
299
300 fs::write(path, content)
301 .await
302 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
303
304 Ok(())
305}
306
307pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
309 match load_config(&path).await {
310 Ok(config) => {
311 tracing::info!("Loaded configuration from {:?}", path.as_ref());
312 config
313 }
314 Err(e) => {
315 tracing::warn!(
316 "Failed to load config from {:?}: {}. Using defaults.",
317 path.as_ref(),
318 e
319 );
320 ServerConfig::default()
321 }
322 }
323}
324
325pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
327 let config = ServerConfig::default();
328 save_config(path, &config).await?;
329 Ok(())
330}
331
332pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
334 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
336 if let Ok(port_num) = port.parse() {
337 config.http.port = port_num;
338 }
339 }
340
341 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
342 config.http.host = host;
343 }
344
345 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
347 if let Ok(port_num) = port.parse() {
348 config.websocket.port = port_num;
349 }
350 }
351
352 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
354 if let Ok(port_num) = port.parse() {
355 config.grpc.port = port_num;
356 }
357 }
358
359 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
361 if let Ok(port_num) = port.parse() {
362 config.smtp.port = port_num;
363 }
364 }
365
366 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
367 config.smtp.host = host;
368 }
369
370 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
371 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
372 }
373
374 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
375 config.smtp.hostname = hostname;
376 }
377
378 if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
380 if let Ok(port_num) = port.parse() {
381 config.tcp.port = port_num;
382 }
383 }
384
385 if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
386 config.tcp.host = host;
387 }
388
389 if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
390 config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
391 }
392
393 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
395 if let Ok(port_num) = port.parse() {
396 config.admin.port = port_num;
397 }
398 }
399
400 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
401 config.admin.enabled = true;
402 }
403
404 if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
406 config.admin.host = host;
407 }
408
409 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
410 if !mount_path.trim().is_empty() {
411 config.admin.mount_path = Some(mount_path);
412 }
413 }
414
415 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
416 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
417 config.admin.api_enabled = on;
418 }
419
420 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
421 config.admin.prometheus_url = prometheus_url;
422 }
423
424 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
426 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
427 config.core.latency_enabled = enabled;
428 }
429
430 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
431 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
432 config.core.failures_enabled = enabled;
433 }
434
435 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
436 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
437 config.core.overrides_enabled = enabled;
438 }
439
440 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
441 let enabled =
442 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
443 config.core.traffic_shaping_enabled = enabled;
444 }
445
446 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
448 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
449 config.core.traffic_shaping.bandwidth.enabled = enabled;
450 }
451
452 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
453 if let Ok(bytes) = max_bytes_per_sec.parse() {
454 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
455 config.core.traffic_shaping.bandwidth.enabled = true;
456 }
457 }
458
459 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
460 if let Ok(bytes) = burst_capacity.parse() {
461 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
462 }
463 }
464
465 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
466 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
467 config.core.traffic_shaping.burst_loss.enabled = enabled;
468 }
469
470 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
471 if let Ok(prob) = burst_probability.parse::<f64>() {
472 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
473 config.core.traffic_shaping.burst_loss.enabled = true;
474 }
475 }
476
477 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
478 if let Ok(ms) = burst_duration.parse() {
479 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
480 }
481 }
482
483 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
484 if let Ok(rate) = loss_rate.parse::<f64>() {
485 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
486 }
487 }
488
489 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
490 if let Ok(ms) = recovery_time.parse() {
491 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
492 }
493 }
494
495 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
497 config.logging.level = level;
498 }
499
500 config
501}
502
503pub fn validate_config(config: &ServerConfig) -> Result<()> {
505 if config.http.port == 0 {
507 return Err(Error::generic("HTTP port cannot be 0"));
508 }
509 if config.websocket.port == 0 {
510 return Err(Error::generic("WebSocket port cannot be 0"));
511 }
512 if config.grpc.port == 0 {
513 return Err(Error::generic("gRPC port cannot be 0"));
514 }
515 if config.admin.port == 0 {
516 return Err(Error::generic("Admin port cannot be 0"));
517 }
518
519 let ports = [
521 ("HTTP", config.http.port),
522 ("WebSocket", config.websocket.port),
523 ("gRPC", config.grpc.port),
524 ("Admin", config.admin.port),
525 ];
526
527 for i in 0..ports.len() {
528 for j in (i + 1)..ports.len() {
529 if ports[i].1 == ports[j].1 {
530 return Err(Error::generic(format!(
531 "Port conflict: {} and {} both use port {}",
532 ports[i].0, ports[j].0, ports[i].1
533 )));
534 }
535 }
536 }
537
538 let valid_levels = ["trace", "debug", "info", "warn", "error"];
540 if !valid_levels.contains(&config.logging.level.as_str()) {
541 return Err(Error::generic(format!(
542 "Invalid log level: {}. Valid levels: {}",
543 config.logging.level,
544 valid_levels.join(", ")
545 )));
546 }
547
548 Ok(())
549}
550
551pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
553 macro_rules! merge_field {
555 ($field:ident) => {
556 if let Some(override_val) = profile.$field {
557 base.$field = override_val;
558 }
559 };
560 }
561
562 merge_field!(http);
563 merge_field!(websocket);
564 merge_field!(graphql);
565 merge_field!(grpc);
566 merge_field!(mqtt);
567 merge_field!(smtp);
568 merge_field!(ftp);
569 merge_field!(kafka);
570 merge_field!(amqp);
571 merge_field!(tcp);
572 merge_field!(admin);
573 merge_field!(chaining);
574 merge_field!(core);
575 merge_field!(logging);
576 merge_field!(data);
577 merge_field!(mockai);
578 merge_field!(observability);
579 merge_field!(multi_tenant);
580 merge_field!(routes);
581 merge_field!(protocols);
582
583 base
584}
585
586pub async fn load_config_with_profile<P: AsRef<Path>>(
588 path: P,
589 profile_name: Option<&str>,
590) -> Result<ServerConfig> {
591 let mut config = load_config_auto(&path).await?;
593
594 if let Some(profile) = profile_name {
596 if let Some(profile_config) = config.profiles.remove(profile) {
597 tracing::info!("Applying profile: {}", profile);
598 config = apply_profile(config, profile_config);
599 } else {
600 return Err(Error::generic(format!(
601 "Profile '{}' not found in configuration. Available profiles: {}",
602 profile,
603 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
604 )));
605 }
606 }
607
608 config.profiles.clear();
610
611 Ok(config)
612}
613
614pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
616 use rquickjs::{Context, Runtime};
617
618 let content = fs::read_to_string(&path)
619 .await
620 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
621
622 let runtime = Runtime::new()
624 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
625 let context = Context::full(&runtime)
626 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
627
628 context.with(|ctx| {
629 let js_content = if path
632 .as_ref()
633 .extension()
634 .and_then(|s| s.to_str())
635 .map(|ext| ext == "ts")
636 .unwrap_or(false)
637 {
638 strip_typescript_types(&content)?
639 } else {
640 content
641 };
642
643 let result: rquickjs::Value = ctx
645 .eval(js_content.as_bytes())
646 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
647
648 let json_str: String = ctx
650 .json_stringify(result)
651 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
652 .ok_or_else(|| Error::generic("JS config returned undefined"))?
653 .get()
654 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
655
656 serde_json::from_str(&json_str).map_err(|e| {
658 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
659 })
660 })
661}
662
663fn strip_typescript_types(content: &str) -> Result<String> {
670 use regex::Regex;
671
672 let mut result = content.to_string();
673
674 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
680 .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
681 result = interface_re.replace_all(&result, "").to_string();
682
683 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
685 .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
686 result = type_alias_re.replace_all(&result, "").to_string();
687
688 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
690 .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
691 result = type_annotation_re.replace_all(&result, "").to_string();
692
693 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
695 .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
696 result = type_import_re.replace_all(&result, "").to_string();
697
698 let as_type_re = Regex::new(r"\s+as\s+\w+")
700 .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
701 result = as_type_re.replace_all(&result, "").to_string();
702
703 Ok(result)
704}
705
706pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
708 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
709
710 match ext {
711 "ts" | "js" => load_config_from_js(&path).await,
712 "yaml" | "yml" | "json" => load_config(&path).await,
713 _ => Err(Error::generic(format!(
714 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
715 ext
716 ))),
717 }
718}
719
720pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
722 let current_dir = std::env::current_dir()
723 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
724
725 let config_names = vec![
726 "mockforge.config.ts",
727 "mockforge.config.js",
728 "mockforge.yaml",
729 "mockforge.yml",
730 ".mockforge.yaml",
731 ".mockforge.yml",
732 ];
733
734 for name in &config_names {
736 let path = current_dir.join(name);
737 if fs::metadata(&path).await.is_ok() {
738 return Ok(path);
739 }
740 }
741
742 let mut dir = current_dir.clone();
744 for _ in 0..5 {
745 if let Some(parent) = dir.parent() {
746 for name in &config_names {
747 let path = parent.join(name);
748 if fs::metadata(&path).await.is_ok() {
749 return Ok(path);
750 }
751 }
752 dir = parent.to_path_buf();
753 } else {
754 break;
755 }
756 }
757
758 Err(Error::generic(
759 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
760 ))
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766
767 #[test]
768 fn test_default_config() {
769 let config = ServerConfig::default();
770 assert_eq!(config.http.port, 3000);
771 assert_eq!(config.websocket.port, 3001);
772 assert_eq!(config.grpc.port, 50051);
773 assert_eq!(config.admin.port, 9080);
774 }
775
776 #[test]
777 fn test_config_validation() {
778 let mut config = ServerConfig::default();
779 assert!(validate_config(&config).is_ok());
780
781 config.websocket.port = config.http.port;
783 assert!(validate_config(&config).is_err());
784
785 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
788 assert!(validate_config(&config).is_err());
789 }
790
791 #[test]
792 fn test_apply_profile() {
793 let base = ServerConfig::default();
794 assert_eq!(base.http.port, 3000);
795
796 let profile = ProfileConfig {
797 http: Some(HttpConfig {
798 port: 8080,
799 ..Default::default()
800 }),
801 logging: Some(LoggingConfig {
802 level: "debug".to_string(),
803 ..Default::default()
804 }),
805 ..Default::default()
806 };
807
808 let merged = apply_profile(base, profile);
809 assert_eq!(merged.http.port, 8080);
810 assert_eq!(merged.logging.level, "debug");
811 assert_eq!(merged.websocket.port, 3001); }
813
814 #[test]
815 fn test_strip_typescript_types() {
816 let ts_code = r#"
817interface Config {
818 port: number;
819 host: string;
820}
821
822const config: Config = {
823 port: 3000,
824 host: "localhost"
825} as Config;
826"#;
827
828 let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
829 assert!(!stripped.contains("interface"));
830 assert!(!stripped.contains(": Config"));
831 assert!(!stripped.contains("as Config"));
832 }
833}