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 #[cfg(feature = "conformance")]
767 pub conformance_state: crate::handlers::conformance::ConformanceState,
768}
769
770impl ManagementState {
771 pub fn new(spec: Option<Arc<OpenApiSpec>>, spec_path: Option<String>, port: u16) -> Self {
778 Self {
779 mocks: Arc::new(RwLock::new(Vec::new())),
780 spec,
781 spec_path,
782 port,
783 start_time: std::time::Instant::now(),
784 request_counter: Arc::new(RwLock::new(0)),
785 proxy_config: None,
786 #[cfg(feature = "smtp")]
787 smtp_registry: None,
788 #[cfg(feature = "mqtt")]
789 mqtt_sessions: None,
790 #[cfg(feature = "kafka")]
791 kafka_broker: None,
792 #[cfg(feature = "amqp")]
793 amqp_broker: None,
794 #[cfg(any(feature = "mqtt", feature = "kafka"))]
795 message_events: {
796 let capacity = get_message_broadcast_capacity();
797 let (tx, _) = broadcast::channel(capacity);
798 Arc::new(tx)
799 },
800 state_machine_manager: Arc::new(RwLock::new(
801 mockforge_scenarios::state_machine::ScenarioStateMachineManager::new(),
802 )),
803 ws_broadcast: None,
804 lifecycle_hooks: None,
805 rule_explanations: Arc::new(RwLock::new(std::collections::HashMap::new())),
806 #[cfg(feature = "chaos")]
807 chaos_api_state: None,
808 server_config: None,
809 #[cfg(feature = "conformance")]
810 conformance_state: crate::handlers::conformance::ConformanceState::new(),
811 }
812 }
813
814 pub fn with_lifecycle_hooks(
816 mut self,
817 hooks: Arc<mockforge_core::lifecycle::LifecycleHookRegistry>,
818 ) -> Self {
819 self.lifecycle_hooks = Some(hooks);
820 self
821 }
822
823 pub fn with_ws_broadcast(
825 mut self,
826 ws_broadcast: Arc<broadcast::Sender<crate::management_ws::MockEvent>>,
827 ) -> Self {
828 self.ws_broadcast = Some(ws_broadcast);
829 self
830 }
831
832 pub fn with_proxy_config(mut self, proxy_config: Arc<RwLock<ProxyConfig>>) -> Self {
834 self.proxy_config = Some(proxy_config);
835 self
836 }
837
838 #[cfg(feature = "smtp")]
839 pub fn with_smtp_registry(
841 mut self,
842 smtp_registry: Arc<mockforge_smtp::SmtpSpecRegistry>,
843 ) -> Self {
844 self.smtp_registry = Some(smtp_registry);
845 self
846 }
847
848 #[cfg(feature = "mqtt")]
849 pub fn with_mqtt_sessions(
851 mut self,
852 mqtt_sessions: Arc<mockforge_mqtt::SessionManager>,
853 ) -> Self {
854 self.mqtt_sessions = Some(mqtt_sessions);
855 self
856 }
857
858 #[cfg(feature = "kafka")]
859 pub fn with_kafka_broker(
861 mut self,
862 kafka_broker: Arc<mockforge_kafka::KafkaMockBroker>,
863 ) -> Self {
864 self.kafka_broker = Some(kafka_broker);
865 self
866 }
867
868 #[cfg(feature = "amqp")]
869 pub fn with_amqp_broker(mut self, amqp_broker: Arc<mockforge_amqp::AmqpBroker>) -> Self {
871 self.amqp_broker = Some(amqp_broker);
872 self
873 }
874
875 #[cfg(feature = "chaos")]
876 pub fn with_chaos_api_state(
878 mut self,
879 chaos_api_state: Arc<mockforge_chaos::api::ChaosApiState>,
880 ) -> Self {
881 self.chaos_api_state = Some(chaos_api_state);
882 self
883 }
884
885 pub fn with_server_config(
887 mut self,
888 server_config: Arc<RwLock<mockforge_core::config::ServerConfig>>,
889 ) -> Self {
890 self.server_config = Some(server_config);
891 self
892 }
893}
894
895pub fn management_router(state: ManagementState) -> Router {
897 let router = Router::new()
898 .route("/capabilities", get(get_capabilities))
899 .route("/health", get(health_check))
900 .route("/stats", get(get_stats))
901 .route("/config", get(get_config))
902 .route("/config/validate", post(validate_config))
903 .route("/config/bulk", post(bulk_update_config))
904 .route("/mocks", get(mocks::list_mocks))
905 .route("/mocks", post(mocks::create_mock))
906 .route("/mocks/{id}", get(mocks::get_mock))
907 .route("/mocks/{id}", put(mocks::update_mock))
908 .route("/mocks/{id}", delete(mocks::delete_mock))
909 .route("/export", get(export_mocks))
910 .route("/import", post(import_mocks))
911 .route("/spec", get(get_openapi_spec))
912 .route("/conformance/violations", get(get_conformance_violations))
918 .route("/conformance/violations", delete(clear_conformance_violations));
919
920 #[cfg(feature = "smtp")]
921 let router = router
922 .route("/smtp/mailbox", get(protocols::list_smtp_emails))
923 .route("/smtp/mailbox", delete(protocols::clear_smtp_mailbox))
924 .route("/smtp/mailbox/{id}", get(protocols::get_smtp_email))
925 .route("/smtp/mailbox/export", get(protocols::export_smtp_mailbox))
926 .route("/smtp/mailbox/search", get(protocols::search_smtp_emails));
927
928 #[cfg(not(feature = "smtp"))]
929 let router = router;
930
931 #[cfg(feature = "mqtt")]
933 let router = router
934 .route("/mqtt/stats", get(protocols::get_mqtt_stats))
935 .route("/mqtt/clients", get(protocols::get_mqtt_clients))
936 .route("/mqtt/topics", get(protocols::get_mqtt_topics))
937 .route("/mqtt/clients/{client_id}", delete(protocols::disconnect_mqtt_client))
938 .route("/mqtt/messages/stream", get(protocols::mqtt_messages_stream))
939 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
940 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
941
942 #[cfg(not(feature = "mqtt"))]
943 let router = router
944 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
945 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
946
947 #[cfg(feature = "kafka")]
948 let router = router
949 .route("/kafka/stats", get(protocols::get_kafka_stats))
950 .route("/kafka/topics", get(protocols::get_kafka_topics))
951 .route("/kafka/topics/{topic}", get(protocols::get_kafka_topic))
952 .route("/kafka/groups", get(protocols::get_kafka_groups))
953 .route("/kafka/groups/{group_id}", get(protocols::get_kafka_group))
954 .route("/kafka/produce", post(protocols::produce_kafka_message))
955 .route("/kafka/produce/batch", post(protocols::produce_kafka_batch))
956 .route("/kafka/messages/stream", get(protocols::kafka_messages_stream));
957
958 #[cfg(not(feature = "kafka"))]
959 let router = router;
960
961 #[cfg(feature = "amqp")]
963 let router = router
964 .route("/amqp/stats", get(protocols::get_amqp_stats))
965 .route("/amqp/exchanges", get(protocols::get_amqp_exchanges))
966 .route("/amqp/exchanges", post(protocols::declare_amqp_exchange))
967 .route("/amqp/exchanges/{name}", delete(protocols::delete_amqp_exchange))
968 .route("/amqp/exchanges/{name}/bindings", post(protocols::add_amqp_binding))
969 .route("/amqp/queues", get(protocols::get_amqp_queues))
970 .route("/amqp/queues", post(protocols::declare_amqp_queue))
971 .route("/amqp/publish", post(protocols::publish_amqp_message));
972
973 #[cfg(not(feature = "amqp"))]
974 let router = router;
975
976 let router = router
978 .route("/migration/routes", get(migration::get_migration_routes))
979 .route("/migration/routes/{pattern}/toggle", post(migration::toggle_route_migration))
980 .route("/migration/routes/{pattern}", put(migration::set_route_migration_mode))
981 .route("/migration/groups/{group}/toggle", post(migration::toggle_group_migration))
982 .route("/migration/groups/{group}", put(migration::set_group_migration_mode))
983 .route("/migration/groups", get(migration::get_migration_groups))
984 .route("/migration/status", get(migration::get_migration_status));
985
986 let router = router
988 .route("/proxy/rules", get(proxy::list_proxy_rules))
989 .route("/proxy/rules", post(proxy::create_proxy_rule))
990 .route("/proxy/rules/{id}", get(proxy::get_proxy_rule))
991 .route("/proxy/rules/{id}", put(proxy::update_proxy_rule))
992 .route("/proxy/rules/{id}", delete(proxy::delete_proxy_rule))
993 .route("/proxy/inspect", get(proxy::get_proxy_inspect));
994
995 let router = router.route("/ai/generate-spec", post(generate_ai_spec));
997
998 let router = router.nest(
1000 "/snapshot-diff",
1001 crate::handlers::snapshot_diff::snapshot_diff_router(state.clone()),
1002 );
1003
1004 #[cfg(feature = "behavioral-cloning")]
1005 let router = router.route("/mockai/generate-openapi", post(generate_openapi_from_traffic));
1006
1007 let router = router
1008 .route("/mockai/learn", post(learn_from_examples))
1009 .route("/mockai/rules/explanations", get(list_rule_explanations))
1010 .route("/mockai/rules/{id}/explanation", get(get_rule_explanation))
1011 .route("/chaos/config", get(get_chaos_config))
1012 .route("/chaos/config", post(update_chaos_config))
1013 .route("/network/profiles", get(list_network_profiles))
1014 .route("/network/profile/apply", post(apply_network_profile));
1015
1016 let router =
1018 router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
1019
1020 #[cfg(feature = "conformance")]
1022 let router = router.nest_service(
1023 "/conformance",
1024 crate::handlers::conformance::conformance_router(state.conformance_state.clone()),
1025 );
1026 #[cfg(not(feature = "conformance"))]
1027 let router = router;
1028
1029 router.with_state(state)
1030}
1031
1032pub fn management_router_with_ui_builder(
1034 state: ManagementState,
1035 server_config: mockforge_core::config::ServerConfig,
1036) -> Router {
1037 use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
1038
1039 let management = management_router(state);
1041
1042 let ui_builder_state = UIBuilderState::new(server_config);
1044 let ui_builder = create_ui_builder_router(ui_builder_state);
1045
1046 management.nest("/ui-builder", ui_builder)
1048}
1049
1050pub fn management_router_with_spec_import(state: ManagementState) -> Router {
1052 use crate::spec_import::{spec_import_router, SpecImportState};
1053
1054 let management = management_router(state);
1056
1057 Router::new()
1059 .merge(management)
1060 .merge(spec_import_router(SpecImportState::new()))
1061}
1062
1063pub async fn serve_dynamic_mock(state: &ManagementState, req: Request<Body>) -> Option<Response> {
1073 let method = req.method().as_str().to_string();
1074 let path = req.uri().path().to_string();
1075
1076 let query_params: std::collections::HashMap<String, String> = req
1077 .uri()
1078 .query()
1079 .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
1080 .unwrap_or_default();
1081
1082 let headers: std::collections::HashMap<String, String> = req
1083 .headers()
1084 .iter()
1085 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str().to_string(), v.to_string())))
1086 .collect();
1087
1088 let (_parts, body) = req.into_parts();
1089 let body_bytes = axum::body::to_bytes(body, 1024 * 1024).await.ok()?;
1090 let body_opt: Option<&[u8]> = if body_bytes.is_empty() {
1091 None
1092 } else {
1093 Some(&body_bytes)
1094 };
1095
1096 let mocks = state.mocks.read().await;
1097
1098 let mut candidates: Vec<&MockConfig> = mocks
1099 .iter()
1100 .filter(|m| mock_matches_request(m, &method, &path, &headers, &query_params, body_opt))
1101 .collect();
1102 if candidates.is_empty() {
1103 return None;
1104 }
1105 candidates.sort_by_key(|m| -(m.priority.unwrap_or(0)));
1106 let mock = candidates.first()?;
1107
1108 if let Some(ms) = mock.latency_ms {
1109 if ms > 0 {
1110 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1111 }
1112 }
1113
1114 let status = mock
1115 .status_code
1116 .and_then(|c| StatusCode::from_u16(c).ok())
1117 .unwrap_or(StatusCode::OK);
1118
1119 let template_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1127 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1128 .unwrap_or(false);
1129 let body_value = if template_expand {
1130 let body_clone = mock.response.body.clone();
1131 match tokio::task::spawn_blocking(move || {
1132 mockforge_core::templating::expand_tokens(&body_clone)
1133 })
1134 .await
1135 {
1136 Ok(expanded) => expanded,
1137 Err(_) => mock.response.body.clone(),
1138 }
1139 } else {
1140 mock.response.body.clone()
1141 };
1142
1143 let body_bytes_out = serde_json::to_vec(&body_value).unwrap_or_default();
1144 let mut response = Response::builder().status(status);
1145
1146 let mut has_content_type = false;
1147 if let Some(h) = &mock.response.headers {
1148 for (k, v) in h {
1149 if k.eq_ignore_ascii_case("content-type") {
1150 has_content_type = true;
1151 }
1152 if let (Ok(name), Ok(value)) =
1153 (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(v))
1154 {
1155 response = response.header(name, value);
1156 }
1157 }
1158 }
1159 if !has_content_type {
1160 response = response.header("content-type", "application/json");
1161 }
1162
1163 Some(
1164 response
1165 .body(Body::from(body_bytes_out))
1166 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
1167 )
1168}
1169
1170pub async fn dynamic_mock_fallback(
1177 State(state): State<ManagementState>,
1178 req: Request<Body>,
1179) -> Response {
1180 let method = req.method().as_str().to_string();
1186 let path = req.uri().path().to_string();
1187 let query = req.uri().query().unwrap_or_default().to_string();
1188
1189 match serve_dynamic_mock(&state, req).await {
1190 Some(resp) => resp,
1191 None => {
1192 let shadow = mockforge_foundation::unknown_paths::shadow_mode_enabled();
1197 let status = if shadow {
1198 StatusCode::OK
1199 } else {
1200 StatusCode::NOT_FOUND
1201 };
1202 mockforge_foundation::unknown_paths::record(
1203 mockforge_foundation::unknown_paths::UnknownPathRequest {
1204 timestamp: chrono::Utc::now(),
1205 method,
1206 path,
1207 client_ip: "unknown".to_string(),
1208 query,
1209 status: status.as_u16(),
1210 },
1211 );
1212 if shadow {
1213 (
1217 StatusCode::OK,
1218 [(http::header::CONTENT_TYPE, "application/json")],
1219 r#"{"shadow":true,"matched":false}"#,
1220 )
1221 .into_response()
1222 } else {
1223 StatusCode::NOT_FOUND.into_response()
1224 }
1225 }
1226 }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231 use super::*;
1232
1233 #[tokio::test]
1234 async fn test_create_and_get_mock() {
1235 let state = ManagementState::new(None, None, 3000);
1236
1237 let mock = MockConfig {
1238 id: "test-1".to_string(),
1239 name: "Test Mock".to_string(),
1240 method: "GET".to_string(),
1241 path: "/test".to_string(),
1242 response: MockResponse {
1243 body: serde_json::json!({"message": "test"}),
1244 headers: None,
1245 },
1246 enabled: true,
1247 latency_ms: None,
1248 status_code: Some(200),
1249 request_match: None,
1250 priority: None,
1251 scenario: None,
1252 required_scenario_state: None,
1253 new_scenario_state: None,
1254 };
1255
1256 {
1258 let mut mocks = state.mocks.write().await;
1259 mocks.push(mock.clone());
1260 }
1261
1262 let mocks = state.mocks.read().await;
1264 let found = mocks.iter().find(|m| m.id == "test-1");
1265 assert!(found.is_some());
1266 assert_eq!(found.unwrap().name, "Test Mock");
1267 }
1268
1269 #[tokio::test]
1270 async fn test_server_stats() {
1271 let state = ManagementState::new(None, None, 3000);
1272
1273 {
1275 let mut mocks = state.mocks.write().await;
1276 mocks.push(MockConfig {
1277 id: "1".to_string(),
1278 name: "Mock 1".to_string(),
1279 method: "GET".to_string(),
1280 path: "/test1".to_string(),
1281 response: MockResponse {
1282 body: serde_json::json!({}),
1283 headers: None,
1284 },
1285 enabled: true,
1286 latency_ms: None,
1287 status_code: Some(200),
1288 request_match: None,
1289 priority: None,
1290 scenario: None,
1291 required_scenario_state: None,
1292 new_scenario_state: None,
1293 });
1294 mocks.push(MockConfig {
1295 id: "2".to_string(),
1296 name: "Mock 2".to_string(),
1297 method: "POST".to_string(),
1298 path: "/test2".to_string(),
1299 response: MockResponse {
1300 body: serde_json::json!({}),
1301 headers: None,
1302 },
1303 enabled: false,
1304 latency_ms: None,
1305 status_code: Some(201),
1306 request_match: None,
1307 priority: None,
1308 scenario: None,
1309 required_scenario_state: None,
1310 new_scenario_state: None,
1311 });
1312 }
1313
1314 let mocks = state.mocks.read().await;
1315 assert_eq!(mocks.len(), 2);
1316 assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
1317 }
1318
1319 #[test]
1320 fn test_mock_matches_request_with_xpath_absolute_path() {
1321 let mock = MockConfig {
1322 id: "xpath-1".to_string(),
1323 name: "XPath Match".to_string(),
1324 method: "POST".to_string(),
1325 path: "/xml".to_string(),
1326 response: MockResponse {
1327 body: serde_json::json!({"ok": true}),
1328 headers: None,
1329 },
1330 enabled: true,
1331 latency_ms: None,
1332 status_code: Some(200),
1333 request_match: Some(RequestMatchCriteria {
1334 xpath: Some("/root/order/id".to_string()),
1335 ..Default::default()
1336 }),
1337 priority: None,
1338 scenario: None,
1339 required_scenario_state: None,
1340 new_scenario_state: None,
1341 };
1342
1343 let body = br#"<root><order><id>123</id></order></root>"#;
1344 let headers = std::collections::HashMap::new();
1345 let query = std::collections::HashMap::new();
1346
1347 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1348 }
1349
1350 #[test]
1351 fn test_mock_matches_request_with_xpath_text_predicate() {
1352 let mock = MockConfig {
1353 id: "xpath-2".to_string(),
1354 name: "XPath Predicate Match".to_string(),
1355 method: "POST".to_string(),
1356 path: "/xml".to_string(),
1357 response: MockResponse {
1358 body: serde_json::json!({"ok": true}),
1359 headers: None,
1360 },
1361 enabled: true,
1362 latency_ms: None,
1363 status_code: Some(200),
1364 request_match: Some(RequestMatchCriteria {
1365 xpath: Some("//order/id[text()='123']".to_string()),
1366 ..Default::default()
1367 }),
1368 priority: None,
1369 scenario: None,
1370 required_scenario_state: None,
1371 new_scenario_state: None,
1372 };
1373
1374 let body = br#"<root><order><id>123</id></order></root>"#;
1375 let headers = std::collections::HashMap::new();
1376 let query = std::collections::HashMap::new();
1377
1378 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1379 }
1380
1381 #[test]
1382 fn test_mock_matches_request_with_xpath_no_match() {
1383 let mock = MockConfig {
1384 id: "xpath-3".to_string(),
1385 name: "XPath No Match".to_string(),
1386 method: "POST".to_string(),
1387 path: "/xml".to_string(),
1388 response: MockResponse {
1389 body: serde_json::json!({"ok": true}),
1390 headers: None,
1391 },
1392 enabled: true,
1393 latency_ms: None,
1394 status_code: Some(200),
1395 request_match: Some(RequestMatchCriteria {
1396 xpath: Some("//order/id[text()='456']".to_string()),
1397 ..Default::default()
1398 }),
1399 priority: None,
1400 scenario: None,
1401 required_scenario_state: None,
1402 new_scenario_state: None,
1403 };
1404
1405 let body = br#"<root><order><id>123</id></order></root>"#;
1406 let headers = std::collections::HashMap::new();
1407 let query = std::collections::HashMap::new();
1408
1409 assert!(!mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1410 }
1411}