1mod ai_gen;
6mod chaos_admin;
7mod conformance;
8mod health;
9mod import_export;
10mod migration;
11mod mocks;
12mod protocols;
13mod proxy;
14mod rule_explanations;
15mod traffic_to_openapi;
16mod workspace_runtime;
17
18pub use ai_gen::*;
21pub use chaos_admin::*;
22pub(crate) use conformance::{clear_conformance_violations, get_conformance_violations};
23pub use health::*;
24pub use import_export::*;
25pub use proxy::{BodyTransformRequest, ProxyRuleRequest, ProxyRuleResponse};
26pub use rule_explanations::*;
27pub use traffic_to_openapi::*;
28
29use axum::{
30 body::Body,
31 extract::State,
32 http::{HeaderName, HeaderValue, Request, StatusCode},
33 response::{IntoResponse, Response},
34 routing::{delete, get, post, put},
35 Router,
36};
37use mockforge_openapi::OpenApiSpec;
38use mockforge_proxy::config::ProxyConfig;
39use serde::{Deserialize, Serialize};
40use std::sync::Arc;
41use tokio::sync::{broadcast, RwLock};
42
43#[cfg(any(feature = "mqtt", feature = "kafka"))]
45const DEFAULT_MESSAGE_BROADCAST_CAPACITY: usize = 1000;
46
47#[cfg(any(feature = "mqtt", feature = "kafka"))]
49fn get_message_broadcast_capacity() -> usize {
50 std::env::var("MOCKFORGE_MESSAGE_BROADCAST_CAPACITY")
51 .ok()
52 .and_then(|s| s.parse().ok())
53 .unwrap_or(DEFAULT_MESSAGE_BROADCAST_CAPACITY)
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(tag = "protocol", content = "data")]
59#[serde(rename_all = "lowercase")]
60pub enum MessageEvent {
61 #[cfg(feature = "mqtt")]
62 Mqtt(MqttMessageEvent),
64 #[cfg(feature = "kafka")]
65 Kafka(KafkaMessageEvent),
67}
68
69#[cfg(feature = "mqtt")]
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct MqttMessageEvent {
73 pub topic: String,
75 pub payload: String,
77 pub qos: u8,
79 pub retain: bool,
81 pub timestamp: String,
83}
84
85#[cfg(feature = "kafka")]
86#[allow(missing_docs)]
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct KafkaMessageEvent {
89 pub topic: String,
90 pub key: Option<String>,
91 pub value: String,
92 pub partition: i32,
93 pub offset: i64,
94 pub headers: Option<std::collections::HashMap<String, String>>,
95 pub timestamp: String,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct MockConfig {
101 #[serde(default, skip_serializing_if = "String::is_empty")]
105 pub id: String,
106 #[serde(default)]
110 pub name: String,
111 pub method: String,
113 pub path: String,
115 pub response: MockResponse,
117 #[serde(default = "default_true")]
119 pub enabled: bool,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub latency_ms: Option<u64>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub status_code: Option<u16>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub request_match: Option<RequestMatchCriteria>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub priority: Option<i32>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub scenario: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub required_scenario_state: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub new_scenario_state: Option<String>,
141}
142
143fn default_true() -> bool {
144 true
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct MockResponse {
150 pub body: serde_json::Value,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub headers: Option<std::collections::HashMap<String, String>>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, Default)]
159#[serde(default)]
160pub struct RequestMatchCriteria {
161 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
163 pub headers: std::collections::HashMap<String, String>,
164 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
166 pub query_params: std::collections::HashMap<String, String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub body_pattern: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub json_path: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub xpath: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub custom_matcher: Option<String>,
179}
180
181pub fn mock_matches_request(
190 mock: &MockConfig,
191 method: &str,
192 path: &str,
193 headers: &std::collections::HashMap<String, String>,
194 query_params: &std::collections::HashMap<String, String>,
195 body: Option<&[u8]>,
196) -> bool {
197 use regex::Regex;
198
199 if !mock.enabled {
201 return false;
202 }
203
204 if mock.method.to_uppercase() != method.to_uppercase() {
206 return false;
207 }
208
209 if !path_matches_pattern(&mock.path, path) {
211 return false;
212 }
213
214 if let Some(criteria) = &mock.request_match {
216 for (key, expected_value) in &criteria.headers {
218 let header_key_lower = key.to_lowercase();
219 let found = headers.iter().find(|(k, _)| k.to_lowercase() == header_key_lower);
220
221 if let Some((_, actual_value)) = found {
222 if let Ok(re) = Regex::new(expected_value) {
224 if !re.is_match(actual_value) {
225 return false;
226 }
227 } else if actual_value != expected_value {
228 return false;
229 }
230 } else {
231 return false; }
233 }
234
235 for (key, expected_value) in &criteria.query_params {
237 if let Some(actual_value) = query_params.get(key) {
238 if actual_value != expected_value {
239 return false;
240 }
241 } else {
242 return false; }
244 }
245
246 if let Some(pattern) = &criteria.body_pattern {
248 if let Some(body_bytes) = body {
249 let body_str = String::from_utf8_lossy(body_bytes);
250 if let Ok(re) = Regex::new(pattern) {
252 if !re.is_match(&body_str) {
253 return false;
254 }
255 } else if body_str.as_ref() != pattern {
256 return false;
257 }
258 } else {
259 return false; }
261 }
262
263 if let Some(json_path) = &criteria.json_path {
265 if let Some(body_bytes) = body {
266 if let Ok(body_str) = std::str::from_utf8(body_bytes) {
267 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(body_str) {
268 if !json_path_exists(&json_value, json_path) {
270 return false;
271 }
272 }
273 }
274 }
275 }
276
277 if let Some(xpath) = &criteria.xpath {
279 if let Some(body_bytes) = body {
280 if let Ok(body_str) = std::str::from_utf8(body_bytes) {
281 if !xml_xpath_exists(body_str, xpath) {
282 return false;
283 }
284 } else {
285 return false;
286 }
287 } else {
288 return false; }
290 }
291
292 if let Some(custom) = &criteria.custom_matcher {
294 if !evaluate_custom_matcher(custom, method, path, headers, query_params, body) {
295 return false;
296 }
297 }
298 }
299
300 true
301}
302
303fn path_matches_pattern(pattern: &str, path: &str) -> bool {
305 if pattern == path {
307 return true;
308 }
309
310 if pattern == "*" {
312 return true;
313 }
314
315 let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
317 let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
318
319 if pattern_parts.len() != path_parts.len() {
320 if pattern.contains('*') {
322 return matches_wildcard_pattern(pattern, path);
323 }
324 return false;
325 }
326
327 for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
328 if pattern_part.starts_with('{') && pattern_part.ends_with('}') {
330 continue; }
332
333 if pattern_part != path_part {
334 return false;
335 }
336 }
337
338 true
339}
340
341fn matches_wildcard_pattern(pattern: &str, path: &str) -> bool {
343 use regex::Regex;
344
345 let regex_pattern = pattern.replace('*', ".*").replace('?', ".?");
347
348 if let Ok(re) = Regex::new(&format!("^{}$", regex_pattern)) {
349 return re.is_match(path);
350 }
351
352 false
353}
354
355fn json_path_exists(json: &serde_json::Value, json_path: &str) -> bool {
363 let path = if json_path == "$" {
364 return true;
365 } else if let Some(p) = json_path.strip_prefix("$.") {
366 p
367 } else if let Some(p) = json_path.strip_prefix('$') {
368 p.strip_prefix('.').unwrap_or(p)
369 } else {
370 json_path
371 };
372
373 let mut current = json;
374 for segment in split_json_path_segments(path) {
375 match segment {
376 JsonPathSegment::Field(name) => {
377 if let Some(obj) = current.as_object() {
378 if let Some(value) = obj.get(name) {
379 current = value;
380 } else {
381 return false;
382 }
383 } else {
384 return false;
385 }
386 }
387 JsonPathSegment::Index(idx) => {
388 if let Some(arr) = current.as_array() {
389 if let Some(value) = arr.get(idx) {
390 current = value;
391 } else {
392 return false;
393 }
394 } else {
395 return false;
396 }
397 }
398 JsonPathSegment::Wildcard => {
399 if let Some(arr) = current.as_array() {
400 return !arr.is_empty();
401 }
402 return false;
403 }
404 }
405 }
406 true
407}
408
409enum JsonPathSegment<'a> {
410 Field(&'a str),
411 Index(usize),
412 Wildcard,
413}
414
415fn split_json_path_segments(path: &str) -> Vec<JsonPathSegment<'_>> {
417 let mut segments = Vec::new();
418 for part in path.split('.') {
419 if part.is_empty() {
420 continue;
421 }
422 if let Some(bracket_start) = part.find('[') {
423 let field_name = &part[..bracket_start];
424 if !field_name.is_empty() {
425 segments.push(JsonPathSegment::Field(field_name));
426 }
427 let bracket_content = &part[bracket_start + 1..part.len() - 1];
428 if bracket_content == "*" {
429 segments.push(JsonPathSegment::Wildcard);
430 } else if let Ok(idx) = bracket_content.parse::<usize>() {
431 segments.push(JsonPathSegment::Index(idx));
432 }
433 } else {
434 segments.push(JsonPathSegment::Field(part));
435 }
436 }
437 segments
438}
439
440#[derive(Debug, Clone, PartialEq, Eq)]
441struct XPathSegment {
442 name: String,
443 text_equals: Option<String>,
444}
445
446fn parse_xpath_segment(segment: &str) -> Option<XPathSegment> {
447 if segment.is_empty() {
448 return None;
449 }
450
451 let trimmed = segment.trim();
452 if let Some(bracket_start) = trimmed.find('[') {
453 if !trimmed.ends_with(']') {
454 return None;
455 }
456
457 let name = trimmed[..bracket_start].trim();
458 let predicate = &trimmed[bracket_start + 1..trimmed.len() - 1];
459 let predicate = predicate.trim();
460
461 if let Some(raw) = predicate.strip_prefix("text()=") {
463 let raw = raw.trim();
464 if raw.len() >= 2
465 && ((raw.starts_with('"') && raw.ends_with('"'))
466 || (raw.starts_with('\'') && raw.ends_with('\'')))
467 {
468 let text = raw[1..raw.len() - 1].to_string();
469 if !name.is_empty() {
470 return Some(XPathSegment {
471 name: name.to_string(),
472 text_equals: Some(text),
473 });
474 }
475 }
476 }
477
478 None
479 } else {
480 Some(XPathSegment {
481 name: trimmed.to_string(),
482 text_equals: None,
483 })
484 }
485}
486
487fn segment_matches(node: roxmltree::Node<'_, '_>, segment: &XPathSegment) -> bool {
488 if !node.is_element() {
489 return false;
490 }
491 if node.tag_name().name() != segment.name {
492 return false;
493 }
494 match &segment.text_equals {
495 Some(expected) => node.text().map(str::trim).unwrap_or_default() == expected,
496 None => true,
497 }
498}
499
500fn xml_xpath_exists(xml_body: &str, xpath: &str) -> bool {
507 let doc = match roxmltree::Document::parse(xml_body) {
508 Ok(doc) => doc,
509 Err(err) => {
510 tracing::warn!("Failed to parse XML for XPath matching: {}", err);
511 return false;
512 }
513 };
514
515 let expr = xpath.trim();
516 if expr.is_empty() {
517 return false;
518 }
519
520 let (is_descendant, path_str) = if let Some(rest) = expr.strip_prefix("//") {
521 (true, rest)
522 } else if let Some(rest) = expr.strip_prefix('/') {
523 (false, rest)
524 } else {
525 tracing::warn!("Unsupported XPath expression (must start with / or //): {}", expr);
526 return false;
527 };
528
529 let segments: Vec<XPathSegment> = path_str
530 .split('/')
531 .filter(|s| !s.trim().is_empty())
532 .filter_map(parse_xpath_segment)
533 .collect();
534
535 if segments.is_empty() {
536 return false;
537 }
538
539 if is_descendant {
540 let first = &segments[0];
541 for node in doc.descendants().filter(|n| segment_matches(*n, first)) {
542 let mut frontier = vec![node];
543 for segment in &segments[1..] {
544 let mut next_frontier = Vec::new();
545 for parent in &frontier {
546 for child in parent.children().filter(|n| segment_matches(*n, segment)) {
547 next_frontier.push(child);
548 }
549 }
550 if next_frontier.is_empty() {
551 frontier.clear();
552 break;
553 }
554 frontier = next_frontier;
555 }
556 if !frontier.is_empty() {
557 return true;
558 }
559 }
560 false
561 } else {
562 let mut frontier = vec![doc.root_element()];
563 for (index, segment) in segments.iter().enumerate() {
564 let mut next_frontier = Vec::new();
565 for parent in &frontier {
566 if index == 0 {
567 if segment_matches(*parent, segment) {
568 next_frontier.push(*parent);
569 }
570 continue;
571 }
572 for child in parent.children().filter(|n| segment_matches(*n, segment)) {
573 next_frontier.push(child);
574 }
575 }
576 if next_frontier.is_empty() {
577 return false;
578 }
579 frontier = next_frontier;
580 }
581 !frontier.is_empty()
582 }
583}
584
585fn evaluate_custom_matcher(
587 expression: &str,
588 method: &str,
589 path: &str,
590 headers: &std::collections::HashMap<String, String>,
591 query_params: &std::collections::HashMap<String, String>,
592 body: Option<&[u8]>,
593) -> bool {
594 use regex::Regex;
595
596 let expr = expression.trim();
597
598 if expr.contains("==") {
600 let parts: Vec<&str> = expr.split("==").map(|s| s.trim()).collect();
601 if parts.len() != 2 {
602 return false;
603 }
604
605 let field = parts[0];
606 let expected_value = parts[1].trim_matches('"').trim_matches('\'');
607
608 match field {
609 "method" => method == expected_value,
610 "path" => path == expected_value,
611 _ if field.starts_with("headers.") => {
612 let header_name = &field[8..];
613 headers.get(header_name).map(|v| v == expected_value).unwrap_or(false)
614 }
615 _ if field.starts_with("query.") => {
616 let param_name = &field[6..];
617 query_params.get(param_name).map(|v| v == expected_value).unwrap_or(false)
618 }
619 _ => false,
620 }
621 }
622 else if expr.contains("=~") {
624 let parts: Vec<&str> = expr.split("=~").map(|s| s.trim()).collect();
625 if parts.len() != 2 {
626 return false;
627 }
628
629 let field = parts[0];
630 let pattern = parts[1].trim_matches('"').trim_matches('\'');
631
632 if let Ok(re) = Regex::new(pattern) {
633 match field {
634 "method" => re.is_match(method),
635 "path" => re.is_match(path),
636 _ if field.starts_with("headers.") => {
637 let header_name = &field[8..];
638 headers.get(header_name).map(|v| re.is_match(v)).unwrap_or(false)
639 }
640 _ if field.starts_with("query.") => {
641 let param_name = &field[6..];
642 query_params.get(param_name).map(|v| re.is_match(v)).unwrap_or(false)
643 }
644 _ => false,
645 }
646 } else {
647 false
648 }
649 }
650 else if expr.contains("contains") {
652 let parts: Vec<&str> = expr.split("contains").map(|s| s.trim()).collect();
653 if parts.len() != 2 {
654 return false;
655 }
656
657 let field = parts[0];
658 let search_value = parts[1].trim_matches('"').trim_matches('\'');
659
660 match field {
661 "path" => path.contains(search_value),
662 _ if field.starts_with("headers.") => {
663 let header_name = &field[8..];
664 headers.get(header_name).map(|v| v.contains(search_value)).unwrap_or(false)
665 }
666 _ if field.starts_with("body") => {
667 if let Some(body_bytes) = body {
668 let body_str = String::from_utf8_lossy(body_bytes);
669 body_str.contains(search_value)
670 } else {
671 false
672 }
673 }
674 _ => false,
675 }
676 } else {
677 tracing::warn!("Unknown custom matcher expression format: {}", expr);
679 false
680 }
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct ServerStats {
686 pub uptime_seconds: u64,
688 pub total_requests: u64,
690 pub active_mocks: usize,
692 pub enabled_mocks: usize,
694 pub registered_routes: usize,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ServerConfig {
701 pub version: String,
703 pub port: u16,
705 pub has_openapi_spec: bool,
707 #[serde(skip_serializing_if = "Option::is_none")]
709 pub spec_path: Option<String>,
710}
711
712#[derive(Clone)]
714pub struct ManagementState {
715 pub mocks: Arc<RwLock<Vec<MockConfig>>>,
717 pub spec: Option<Arc<OpenApiSpec>>,
719 pub spec_path: Option<String>,
721 pub port: u16,
723 pub start_time: std::time::Instant,
725 pub request_counter: Arc<RwLock<u64>>,
727 pub proxy_config: Option<Arc<RwLock<ProxyConfig>>>,
729 #[cfg(feature = "smtp")]
731 pub smtp_registry: Option<Arc<mockforge_smtp::SmtpSpecRegistry>>,
732 #[cfg(feature = "mqtt")]
736 pub mqtt_sessions: Option<Arc<mockforge_mqtt::SessionManager>>,
737 #[cfg(feature = "kafka")]
739 pub kafka_broker: Option<Arc<mockforge_kafka::KafkaMockBroker>>,
740 #[cfg(feature = "amqp")]
742 pub amqp_broker: Option<Arc<mockforge_amqp::AmqpBroker>>,
743 #[cfg(any(feature = "mqtt", feature = "kafka"))]
745 pub message_events: Arc<broadcast::Sender<MessageEvent>>,
746 #[cfg(feature = "scenario-engine")]
748 pub state_machine_manager:
749 Arc<RwLock<mockforge_scenarios::state_machine::ScenarioStateMachineManager>>,
750 pub ws_broadcast: Option<Arc<broadcast::Sender<crate::management_ws::MockEvent>>>,
752 pub lifecycle_hooks: Option<Arc<mockforge_core::lifecycle::LifecycleHookRegistry>>,
754 pub rule_explanations: Arc<
756 RwLock<
757 std::collections::HashMap<
758 String,
759 mockforge_foundation::intelligent_behavior::rule_types::RuleExplanation,
760 >,
761 >,
762 >,
763 #[cfg(feature = "chaos")]
765 pub chaos_api_state: Option<Arc<mockforge_chaos::api::ChaosApiState>>,
766 pub server_config: Option<Arc<RwLock<mockforge_core::config::ServerConfig>>>,
768 pub base_path: Option<String>,
776 #[cfg(feature = "conformance")]
778 pub conformance_state: crate::handlers::conformance::ConformanceState,
779}
780
781impl ManagementState {
782 pub fn new(spec: Option<Arc<OpenApiSpec>>, spec_path: Option<String>, port: u16) -> Self {
789 Self {
790 mocks: Arc::new(RwLock::new(Vec::new())),
791 spec,
792 spec_path,
793 port,
794 start_time: std::time::Instant::now(),
795 request_counter: Arc::new(RwLock::new(0)),
796 proxy_config: None,
797 #[cfg(feature = "smtp")]
798 smtp_registry: None,
799 #[cfg(feature = "mqtt")]
800 mqtt_sessions: None,
801 #[cfg(feature = "kafka")]
802 kafka_broker: None,
803 #[cfg(feature = "amqp")]
804 amqp_broker: None,
805 #[cfg(any(feature = "mqtt", feature = "kafka"))]
806 message_events: {
807 let capacity = get_message_broadcast_capacity();
808 let (tx, _) = broadcast::channel(capacity);
809 Arc::new(tx)
810 },
811 #[cfg(feature = "scenario-engine")]
812 state_machine_manager: Arc::new(RwLock::new(
813 mockforge_scenarios::state_machine::ScenarioStateMachineManager::new(),
814 )),
815 ws_broadcast: None,
816 lifecycle_hooks: None,
817 rule_explanations: Arc::new(RwLock::new(std::collections::HashMap::new())),
818 #[cfg(feature = "chaos")]
819 chaos_api_state: None,
820 server_config: None,
821 base_path: std::env::var("MOCKFORGE_API_BASE_PATH").ok().and_then(|p| {
825 let trimmed = p.trim_end_matches('/').to_string();
826 if trimmed.is_empty() || trimmed == "/" {
827 None
828 } else {
829 Some(trimmed)
830 }
831 }),
832 #[cfg(feature = "conformance")]
833 conformance_state: crate::handlers::conformance::ConformanceState::new(),
834 }
835 }
836
837 pub fn with_base_path(mut self, base_path: Option<String>) -> Self {
841 self.base_path = base_path.and_then(|p| {
842 let trimmed = p.trim_end_matches('/').to_string();
843 if trimmed.is_empty() || trimmed == "/" {
844 None
845 } else {
846 Some(trimmed)
847 }
848 });
849 self
850 }
851
852 pub fn with_lifecycle_hooks(
854 mut self,
855 hooks: Arc<mockforge_core::lifecycle::LifecycleHookRegistry>,
856 ) -> Self {
857 self.lifecycle_hooks = Some(hooks);
858 self
859 }
860
861 pub fn with_ws_broadcast(
863 mut self,
864 ws_broadcast: Arc<broadcast::Sender<crate::management_ws::MockEvent>>,
865 ) -> Self {
866 self.ws_broadcast = Some(ws_broadcast);
867 self
868 }
869
870 pub fn with_proxy_config(mut self, proxy_config: Arc<RwLock<ProxyConfig>>) -> Self {
872 self.proxy_config = Some(proxy_config);
873 self
874 }
875
876 #[cfg(feature = "smtp")]
877 pub fn with_smtp_registry(
879 mut self,
880 smtp_registry: Arc<mockforge_smtp::SmtpSpecRegistry>,
881 ) -> Self {
882 self.smtp_registry = Some(smtp_registry);
883 self
884 }
885
886 #[cfg(feature = "mqtt")]
887 pub fn with_mqtt_sessions(
889 mut self,
890 mqtt_sessions: Arc<mockforge_mqtt::SessionManager>,
891 ) -> Self {
892 self.mqtt_sessions = Some(mqtt_sessions);
893 self
894 }
895
896 #[cfg(feature = "kafka")]
897 pub fn with_kafka_broker(
899 mut self,
900 kafka_broker: Arc<mockforge_kafka::KafkaMockBroker>,
901 ) -> Self {
902 self.kafka_broker = Some(kafka_broker);
903 self
904 }
905
906 #[cfg(feature = "amqp")]
907 pub fn with_amqp_broker(mut self, amqp_broker: Arc<mockforge_amqp::AmqpBroker>) -> Self {
909 self.amqp_broker = Some(amqp_broker);
910 self
911 }
912
913 #[cfg(feature = "chaos")]
914 pub fn with_chaos_api_state(
916 mut self,
917 chaos_api_state: Arc<mockforge_chaos::api::ChaosApiState>,
918 ) -> Self {
919 self.chaos_api_state = Some(chaos_api_state);
920 self
921 }
922
923 pub fn with_server_config(
925 mut self,
926 server_config: Arc<RwLock<mockforge_core::config::ServerConfig>>,
927 ) -> Self {
928 self.server_config = Some(server_config);
929 self
930 }
931}
932
933pub fn management_router(state: ManagementState) -> Router {
935 let router = Router::new()
936 .route("/capabilities", get(get_capabilities))
937 .route("/health", get(health_check))
938 .route("/stats", get(get_stats))
939 .route("/config", get(get_config))
940 .route("/config/validate", post(validate_config))
941 .route("/config/bulk", post(bulk_update_config))
942 .route("/workspaces/{workspace_id}/activate", post(workspace_runtime::activate_workspace_mocks))
943 .route("/mocks", get(mocks::list_mocks))
944 .route("/mocks", post(mocks::create_mock))
945 .route("/mocks/{id}", get(mocks::get_mock))
946 .route("/mocks/{id}", put(mocks::update_mock))
947 .route("/mocks/{id}", delete(mocks::delete_mock))
948 .route("/export", get(export_mocks))
949 .route("/import", post(import_mocks))
950 .route("/spec", get(get_openapi_spec))
951 .route("/conformance/violations", get(get_conformance_violations))
957 .route("/conformance/violations", delete(clear_conformance_violations));
958
959 #[cfg(feature = "smtp")]
960 let router = router
961 .route("/smtp/mailbox", get(protocols::list_smtp_emails))
962 .route("/smtp/mailbox", delete(protocols::clear_smtp_mailbox))
963 .route("/smtp/mailbox/{id}", get(protocols::get_smtp_email))
964 .route("/smtp/mailbox/export", get(protocols::export_smtp_mailbox))
965 .route("/smtp/mailbox/search", get(protocols::search_smtp_emails));
966
967 #[cfg(not(feature = "smtp"))]
968 let router = router;
969
970 #[cfg(feature = "mqtt")]
972 let router = router
973 .route("/mqtt/stats", get(protocols::get_mqtt_stats))
974 .route("/mqtt/clients", get(protocols::get_mqtt_clients))
975 .route("/mqtt/topics", get(protocols::get_mqtt_topics))
976 .route("/mqtt/clients/{client_id}", delete(protocols::disconnect_mqtt_client))
977 .route("/mqtt/messages/stream", get(protocols::mqtt_messages_stream))
978 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
979 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
980
981 #[cfg(not(feature = "mqtt"))]
982 let router = router
983 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
984 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
985
986 #[cfg(feature = "kafka")]
987 let router = router
988 .route("/kafka/stats", get(protocols::get_kafka_stats))
989 .route("/kafka/topics", get(protocols::get_kafka_topics))
990 .route("/kafka/topics/{topic}", get(protocols::get_kafka_topic))
991 .route("/kafka/groups", get(protocols::get_kafka_groups))
992 .route("/kafka/groups/{group_id}", get(protocols::get_kafka_group))
993 .route("/kafka/produce", post(protocols::produce_kafka_message))
994 .route("/kafka/produce/batch", post(protocols::produce_kafka_batch))
995 .route("/kafka/messages/stream", get(protocols::kafka_messages_stream));
996
997 #[cfg(not(feature = "kafka"))]
998 let router = router;
999
1000 #[cfg(feature = "amqp")]
1002 let router = router
1003 .route("/amqp/stats", get(protocols::get_amqp_stats))
1004 .route("/amqp/exchanges", get(protocols::get_amqp_exchanges))
1005 .route("/amqp/exchanges", post(protocols::declare_amqp_exchange))
1006 .route("/amqp/exchanges/{name}", delete(protocols::delete_amqp_exchange))
1007 .route("/amqp/exchanges/{name}/bindings", post(protocols::add_amqp_binding))
1008 .route("/amqp/queues", get(protocols::get_amqp_queues))
1009 .route("/amqp/queues", post(protocols::declare_amqp_queue))
1010 .route("/amqp/publish", post(protocols::publish_amqp_message));
1011
1012 #[cfg(not(feature = "amqp"))]
1013 let router = router;
1014
1015 let router = router
1017 .route("/migration/routes", get(migration::get_migration_routes))
1018 .route("/migration/routes/{pattern}/toggle", post(migration::toggle_route_migration))
1019 .route("/migration/routes/{pattern}", put(migration::set_route_migration_mode))
1020 .route("/migration/groups/{group}/toggle", post(migration::toggle_group_migration))
1021 .route("/migration/groups/{group}", put(migration::set_group_migration_mode))
1022 .route("/migration/groups", get(migration::get_migration_groups))
1023 .route("/migration/status", get(migration::get_migration_status));
1024
1025 let router = router
1027 .route("/proxy/rules", get(proxy::list_proxy_rules))
1028 .route("/proxy/rules", post(proxy::create_proxy_rule))
1029 .route("/proxy/rules/{id}", get(proxy::get_proxy_rule))
1030 .route("/proxy/rules/{id}", put(proxy::update_proxy_rule))
1031 .route("/proxy/rules/{id}", delete(proxy::delete_proxy_rule))
1032 .route("/proxy/inspect", get(proxy::get_proxy_inspect));
1033
1034 let router = router.route("/ai/generate-spec", post(generate_ai_spec));
1036
1037 let router = router.nest(
1039 "/snapshot-diff",
1040 crate::handlers::snapshot_diff::snapshot_diff_router(state.clone()),
1041 );
1042
1043 #[cfg(feature = "behavioral-cloning")]
1044 let router = router.route("/mockai/generate-openapi", post(generate_openapi_from_traffic));
1045
1046 let router = router
1047 .route("/mockai/learn", post(learn_from_examples))
1048 .route("/mockai/rules/explanations", get(list_rule_explanations))
1049 .route("/mockai/rules/{id}/explanation", get(get_rule_explanation))
1050 .route("/chaos/config", get(get_chaos_config))
1051 .route("/chaos/config", post(update_chaos_config))
1052 .route("/network/profiles", get(list_network_profiles))
1053 .route("/network/profile/apply", post(apply_network_profile));
1054
1055 #[cfg(feature = "scenario-engine")]
1057 let router =
1058 router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
1059
1060 #[cfg(feature = "conformance")]
1062 let router = router.nest_service(
1063 "/conformance",
1064 crate::handlers::conformance::conformance_router(state.conformance_state.clone()),
1065 );
1066 #[cfg(not(feature = "conformance"))]
1067 let router = router;
1068
1069 router.with_state(state)
1070}
1071
1072pub fn management_router_with_ui_builder(
1074 state: ManagementState,
1075 server_config: mockforge_core::config::ServerConfig,
1076) -> Router {
1077 use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
1078
1079 let management = management_router(state);
1081
1082 let ui_builder_state = UIBuilderState::new(server_config);
1084 let ui_builder = create_ui_builder_router(ui_builder_state);
1085
1086 management.nest("/ui-builder", ui_builder)
1088}
1089
1090pub fn management_router_with_spec_import(state: ManagementState) -> Router {
1092 use crate::spec_import::{spec_import_router, SpecImportState};
1093
1094 let management = management_router(state);
1096
1097 Router::new()
1099 .merge(management)
1100 .merge(spec_import_router(SpecImportState::new()))
1101}
1102
1103pub async fn serve_dynamic_mock(state: &ManagementState, req: Request<Body>) -> Option<Response> {
1113 let method = req.method().as_str().to_string();
1114 let path = req.uri().path().to_string();
1115
1116 let query_params: std::collections::HashMap<String, String> = req
1117 .uri()
1118 .query()
1119 .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
1120 .unwrap_or_default();
1121
1122 let headers: std::collections::HashMap<String, String> = req
1123 .headers()
1124 .iter()
1125 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str().to_string(), v.to_string())))
1126 .collect();
1127
1128 let (_parts, body) = req.into_parts();
1129 let body_bytes = axum::body::to_bytes(body, 1024 * 1024).await.ok()?;
1130 let body_opt: Option<&[u8]> = if body_bytes.is_empty() {
1131 None
1132 } else {
1133 Some(&body_bytes)
1134 };
1135
1136 let mocks = state.mocks.read().await;
1137
1138 let mut candidates: Vec<&MockConfig> = mocks
1139 .iter()
1140 .filter(|m| mock_matches_request(m, &method, &path, &headers, &query_params, body_opt))
1141 .collect();
1142 if candidates.is_empty() {
1143 return None;
1144 }
1145 candidates.sort_by_key(|m| -(m.priority.unwrap_or(0)));
1146 let mock = candidates.first()?;
1147
1148 if let Some(ms) = mock.latency_ms {
1149 if ms > 0 {
1150 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1151 }
1152 }
1153
1154 let status = mock
1155 .status_code
1156 .and_then(|c| StatusCode::from_u16(c).ok())
1157 .unwrap_or(StatusCode::OK);
1158
1159 let template_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1167 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1168 .unwrap_or(false);
1169 let body_value = if template_expand {
1170 let body_clone = mock.response.body.clone();
1171 match tokio::task::spawn_blocking(move || {
1172 mockforge_core::templating::expand_tokens(&body_clone)
1173 })
1174 .await
1175 {
1176 Ok(expanded) => expanded,
1177 Err(_) => mock.response.body.clone(),
1178 }
1179 } else {
1180 mock.response.body.clone()
1181 };
1182
1183 let body_bytes_out = if mock.id.starts_with(workspace_runtime::WORKSPACE_MOCK_PREFIX) {
1184 body_value
1185 .as_str()
1186 .map(|body| body.as_bytes().to_vec())
1187 .unwrap_or_else(|| serde_json::to_vec(&body_value).unwrap_or_default())
1188 } else {
1189 serde_json::to_vec(&body_value).unwrap_or_default()
1190 };
1191 let mut response = Response::builder().status(status);
1192
1193 let mut has_content_type = false;
1194 if let Some(h) = &mock.response.headers {
1195 for (k, v) in h {
1196 if k.eq_ignore_ascii_case("content-type") {
1197 has_content_type = true;
1198 }
1199 if let (Ok(name), Ok(value)) =
1200 (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(v))
1201 {
1202 response = response.header(name, value);
1203 }
1204 }
1205 }
1206 if !has_content_type {
1207 response = response.header("content-type", "application/json");
1208 }
1209
1210 Some(
1211 response
1212 .body(Body::from(body_bytes_out))
1213 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
1214 )
1215}
1216
1217pub async fn dynamic_mock_fallback(
1224 State(state): State<ManagementState>,
1225 req: Request<Body>,
1226) -> Response {
1227 let method = req.method().as_str().to_string();
1233 let path = req.uri().path().to_string();
1234 let query = req.uri().query().unwrap_or_default().to_string();
1235
1236 match serve_dynamic_mock(&state, req).await {
1237 Some(resp) => resp,
1238 None => {
1239 let shadow_enabled = mockforge_foundation::unknown_paths::shadow_mode_enabled();
1252 let in_base_path = match state.base_path.as_deref() {
1253 Some(bp) => path_in_base(&path, bp),
1254 None => true, };
1256 let shadow = shadow_enabled && in_base_path;
1257 let status = if shadow {
1258 StatusCode::OK
1259 } else {
1260 StatusCode::NOT_FOUND
1261 };
1262 mockforge_foundation::unknown_paths::record(
1263 mockforge_foundation::unknown_paths::UnknownPathRequest {
1264 timestamp: chrono::Utc::now(),
1265 method,
1266 path,
1267 client_ip: "unknown".to_string(),
1268 query,
1269 status: status.as_u16(),
1270 },
1271 );
1272 if shadow {
1273 (
1277 StatusCode::OK,
1278 [(http::header::CONTENT_TYPE, "application/json")],
1279 r#"{"shadow":true,"matched":false}"#,
1280 )
1281 .into_response()
1282 } else {
1283 StatusCode::NOT_FOUND.into_response()
1284 }
1285 }
1286 }
1287}
1288
1289pub(crate) fn path_in_base(path: &str, base_path: &str) -> bool {
1296 let bp = base_path.trim_end_matches('/');
1297 if bp.is_empty() {
1298 return true;
1299 }
1300 if path == bp {
1301 return true;
1302 }
1303 path.starts_with(bp) && path.as_bytes().get(bp.len()).copied() == Some(b'/')
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309 use super::*;
1310
1311 #[test]
1316 fn path_in_base_segment_boundary() {
1317 assert!(path_in_base("/api", "/api"));
1319 assert!(path_in_base("/api/users", "/api"));
1321 assert!(path_in_base("/api/v1/items/42", "/api"));
1322 assert!(!path_in_base("/api123", "/api"));
1324 assert!(!path_in_base("/api123/foo", "/api"));
1325 assert!(!path_in_base("/", "/api"));
1327 assert!(!path_in_base("/admin", "/api"));
1328 assert!(path_in_base("/api/users", "/api/"));
1330 assert!(path_in_base("/anything", ""));
1332 }
1333
1334 #[test]
1337 fn with_base_path_normalises() {
1338 let s = ManagementState::new(None, None, 3000).with_base_path(Some("/api".to_string()));
1339 assert_eq!(s.base_path.as_deref(), Some("/api"));
1340 let s = ManagementState::new(None, None, 3000).with_base_path(Some("/api/".to_string()));
1341 assert_eq!(s.base_path.as_deref(), Some("/api"));
1342 let s = ManagementState::new(None, None, 3000).with_base_path(Some("".to_string()));
1343 assert_eq!(s.base_path.as_deref(), None);
1344 let s = ManagementState::new(None, None, 3000).with_base_path(Some("/".to_string()));
1345 assert_eq!(s.base_path.as_deref(), None);
1346 let s = ManagementState::new(None, None, 3000).with_base_path(None);
1347 assert_eq!(s.base_path.as_deref(), None);
1348 }
1349
1350 #[tokio::test]
1351 async fn test_create_and_get_mock() {
1352 let state = ManagementState::new(None, None, 3000);
1353
1354 let mock = MockConfig {
1355 id: "test-1".to_string(),
1356 name: "Test Mock".to_string(),
1357 method: "GET".to_string(),
1358 path: "/test".to_string(),
1359 response: MockResponse {
1360 body: serde_json::json!({"message": "test"}),
1361 headers: None,
1362 },
1363 enabled: true,
1364 latency_ms: None,
1365 status_code: Some(200),
1366 request_match: None,
1367 priority: None,
1368 scenario: None,
1369 required_scenario_state: None,
1370 new_scenario_state: None,
1371 };
1372
1373 {
1375 let mut mocks = state.mocks.write().await;
1376 mocks.push(mock.clone());
1377 }
1378
1379 let mocks = state.mocks.read().await;
1381 let found = mocks.iter().find(|m| m.id == "test-1");
1382 assert!(found.is_some());
1383 assert_eq!(found.unwrap().name, "Test Mock");
1384 }
1385
1386 #[tokio::test]
1387 async fn test_server_stats() {
1388 let state = ManagementState::new(None, None, 3000);
1389
1390 {
1392 let mut mocks = state.mocks.write().await;
1393 mocks.push(MockConfig {
1394 id: "1".to_string(),
1395 name: "Mock 1".to_string(),
1396 method: "GET".to_string(),
1397 path: "/test1".to_string(),
1398 response: MockResponse {
1399 body: serde_json::json!({}),
1400 headers: None,
1401 },
1402 enabled: true,
1403 latency_ms: None,
1404 status_code: Some(200),
1405 request_match: None,
1406 priority: None,
1407 scenario: None,
1408 required_scenario_state: None,
1409 new_scenario_state: None,
1410 });
1411 mocks.push(MockConfig {
1412 id: "2".to_string(),
1413 name: "Mock 2".to_string(),
1414 method: "POST".to_string(),
1415 path: "/test2".to_string(),
1416 response: MockResponse {
1417 body: serde_json::json!({}),
1418 headers: None,
1419 },
1420 enabled: false,
1421 latency_ms: None,
1422 status_code: Some(201),
1423 request_match: None,
1424 priority: None,
1425 scenario: None,
1426 required_scenario_state: None,
1427 new_scenario_state: None,
1428 });
1429 }
1430
1431 let mocks = state.mocks.read().await;
1432 assert_eq!(mocks.len(), 2);
1433 assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
1434 }
1435
1436 #[test]
1437 fn test_mock_matches_request_with_xpath_absolute_path() {
1438 let mock = MockConfig {
1439 id: "xpath-1".to_string(),
1440 name: "XPath Match".to_string(),
1441 method: "POST".to_string(),
1442 path: "/xml".to_string(),
1443 response: MockResponse {
1444 body: serde_json::json!({"ok": true}),
1445 headers: None,
1446 },
1447 enabled: true,
1448 latency_ms: None,
1449 status_code: Some(200),
1450 request_match: Some(RequestMatchCriteria {
1451 xpath: Some("/root/order/id".to_string()),
1452 ..Default::default()
1453 }),
1454 priority: None,
1455 scenario: None,
1456 required_scenario_state: None,
1457 new_scenario_state: None,
1458 };
1459
1460 let body = br#"<root><order><id>123</id></order></root>"#;
1461 let headers = std::collections::HashMap::new();
1462 let query = std::collections::HashMap::new();
1463
1464 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1465 }
1466
1467 #[test]
1468 fn test_mock_matches_request_with_xpath_text_predicate() {
1469 let mock = MockConfig {
1470 id: "xpath-2".to_string(),
1471 name: "XPath Predicate Match".to_string(),
1472 method: "POST".to_string(),
1473 path: "/xml".to_string(),
1474 response: MockResponse {
1475 body: serde_json::json!({"ok": true}),
1476 headers: None,
1477 },
1478 enabled: true,
1479 latency_ms: None,
1480 status_code: Some(200),
1481 request_match: Some(RequestMatchCriteria {
1482 xpath: Some("//order/id[text()='123']".to_string()),
1483 ..Default::default()
1484 }),
1485 priority: None,
1486 scenario: None,
1487 required_scenario_state: None,
1488 new_scenario_state: None,
1489 };
1490
1491 let body = br#"<root><order><id>123</id></order></root>"#;
1492 let headers = std::collections::HashMap::new();
1493 let query = std::collections::HashMap::new();
1494
1495 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1496 }
1497
1498 #[test]
1499 fn test_mock_matches_request_with_xpath_no_match() {
1500 let mock = MockConfig {
1501 id: "xpath-3".to_string(),
1502 name: "XPath No Match".to_string(),
1503 method: "POST".to_string(),
1504 path: "/xml".to_string(),
1505 response: MockResponse {
1506 body: serde_json::json!({"ok": true}),
1507 headers: None,
1508 },
1509 enabled: true,
1510 latency_ms: None,
1511 status_code: Some(200),
1512 request_match: Some(RequestMatchCriteria {
1513 xpath: Some("//order/id[text()='456']".to_string()),
1514 ..Default::default()
1515 }),
1516 priority: None,
1517 scenario: None,
1518 required_scenario_state: None,
1519 new_scenario_state: None,
1520 };
1521
1522 let body = br#"<root><order><id>123</id></order></root>"#;
1523 let headers = std::collections::HashMap::new();
1524 let query = std::collections::HashMap::new();
1525
1526 assert!(!mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1527 }
1528}