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