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")]
732 pub mqtt_broker: Option<Arc<mockforge_mqtt::MqttBroker>>,
733 #[cfg(feature = "kafka")]
735 pub kafka_broker: Option<Arc<mockforge_kafka::KafkaMockBroker>>,
736 #[cfg(any(feature = "mqtt", feature = "kafka"))]
738 pub message_events: Arc<broadcast::Sender<MessageEvent>>,
739 pub state_machine_manager:
741 Arc<RwLock<mockforge_scenarios::state_machine::ScenarioStateMachineManager>>,
742 pub ws_broadcast: Option<Arc<broadcast::Sender<crate::management_ws::MockEvent>>>,
744 pub lifecycle_hooks: Option<Arc<mockforge_core::lifecycle::LifecycleHookRegistry>>,
746 pub rule_explanations: Arc<
748 RwLock<
749 std::collections::HashMap<
750 String,
751 mockforge_foundation::intelligent_behavior::rule_types::RuleExplanation,
752 >,
753 >,
754 >,
755 #[cfg(feature = "chaos")]
757 pub chaos_api_state: Option<Arc<mockforge_chaos::api::ChaosApiState>>,
758 pub server_config: Option<Arc<RwLock<mockforge_core::config::ServerConfig>>>,
760 #[cfg(feature = "conformance")]
762 pub conformance_state: crate::handlers::conformance::ConformanceState,
763}
764
765impl ManagementState {
766 pub fn new(spec: Option<Arc<OpenApiSpec>>, spec_path: Option<String>, port: u16) -> Self {
773 Self {
774 mocks: Arc::new(RwLock::new(Vec::new())),
775 spec,
776 spec_path,
777 port,
778 start_time: std::time::Instant::now(),
779 request_counter: Arc::new(RwLock::new(0)),
780 proxy_config: None,
781 #[cfg(feature = "smtp")]
782 smtp_registry: None,
783 #[cfg(feature = "mqtt")]
784 mqtt_broker: None,
785 #[cfg(feature = "kafka")]
786 kafka_broker: None,
787 #[cfg(any(feature = "mqtt", feature = "kafka"))]
788 message_events: {
789 let capacity = get_message_broadcast_capacity();
790 let (tx, _) = broadcast::channel(capacity);
791 Arc::new(tx)
792 },
793 state_machine_manager: Arc::new(RwLock::new(
794 mockforge_scenarios::state_machine::ScenarioStateMachineManager::new(),
795 )),
796 ws_broadcast: None,
797 lifecycle_hooks: None,
798 rule_explanations: Arc::new(RwLock::new(std::collections::HashMap::new())),
799 #[cfg(feature = "chaos")]
800 chaos_api_state: None,
801 server_config: None,
802 #[cfg(feature = "conformance")]
803 conformance_state: crate::handlers::conformance::ConformanceState::new(),
804 }
805 }
806
807 pub fn with_lifecycle_hooks(
809 mut self,
810 hooks: Arc<mockforge_core::lifecycle::LifecycleHookRegistry>,
811 ) -> Self {
812 self.lifecycle_hooks = Some(hooks);
813 self
814 }
815
816 pub fn with_ws_broadcast(
818 mut self,
819 ws_broadcast: Arc<broadcast::Sender<crate::management_ws::MockEvent>>,
820 ) -> Self {
821 self.ws_broadcast = Some(ws_broadcast);
822 self
823 }
824
825 pub fn with_proxy_config(mut self, proxy_config: Arc<RwLock<ProxyConfig>>) -> Self {
827 self.proxy_config = Some(proxy_config);
828 self
829 }
830
831 #[cfg(feature = "smtp")]
832 pub fn with_smtp_registry(
834 mut self,
835 smtp_registry: Arc<mockforge_smtp::SmtpSpecRegistry>,
836 ) -> Self {
837 self.smtp_registry = Some(smtp_registry);
838 self
839 }
840
841 #[cfg(feature = "mqtt")]
842 pub fn with_mqtt_broker(mut self, mqtt_broker: Arc<mockforge_mqtt::MqttBroker>) -> Self {
844 self.mqtt_broker = Some(mqtt_broker);
845 self
846 }
847
848 #[cfg(feature = "kafka")]
849 pub fn with_kafka_broker(
851 mut self,
852 kafka_broker: Arc<mockforge_kafka::KafkaMockBroker>,
853 ) -> Self {
854 self.kafka_broker = Some(kafka_broker);
855 self
856 }
857
858 #[cfg(feature = "chaos")]
859 pub fn with_chaos_api_state(
861 mut self,
862 chaos_api_state: Arc<mockforge_chaos::api::ChaosApiState>,
863 ) -> Self {
864 self.chaos_api_state = Some(chaos_api_state);
865 self
866 }
867
868 pub fn with_server_config(
870 mut self,
871 server_config: Arc<RwLock<mockforge_core::config::ServerConfig>>,
872 ) -> Self {
873 self.server_config = Some(server_config);
874 self
875 }
876}
877
878pub fn management_router(state: ManagementState) -> Router {
880 let router = Router::new()
881 .route("/capabilities", get(get_capabilities))
882 .route("/health", get(health_check))
883 .route("/stats", get(get_stats))
884 .route("/config", get(get_config))
885 .route("/config/validate", post(validate_config))
886 .route("/config/bulk", post(bulk_update_config))
887 .route("/mocks", get(mocks::list_mocks))
888 .route("/mocks", post(mocks::create_mock))
889 .route("/mocks/{id}", get(mocks::get_mock))
890 .route("/mocks/{id}", put(mocks::update_mock))
891 .route("/mocks/{id}", delete(mocks::delete_mock))
892 .route("/export", get(export_mocks))
893 .route("/import", post(import_mocks))
894 .route("/spec", get(get_openapi_spec))
895 .route("/conformance/violations", get(get_conformance_violations))
901 .route("/conformance/violations", delete(clear_conformance_violations));
902
903 #[cfg(feature = "smtp")]
904 let router = router
905 .route("/smtp/mailbox", get(protocols::list_smtp_emails))
906 .route("/smtp/mailbox", delete(protocols::clear_smtp_mailbox))
907 .route("/smtp/mailbox/{id}", get(protocols::get_smtp_email))
908 .route("/smtp/mailbox/export", get(protocols::export_smtp_mailbox))
909 .route("/smtp/mailbox/search", get(protocols::search_smtp_emails));
910
911 #[cfg(not(feature = "smtp"))]
912 let router = router;
913
914 #[cfg(feature = "mqtt")]
916 let router = router
917 .route("/mqtt/stats", get(protocols::get_mqtt_stats))
918 .route("/mqtt/clients", get(protocols::get_mqtt_clients))
919 .route("/mqtt/topics", get(protocols::get_mqtt_topics))
920 .route("/mqtt/clients/{client_id}", delete(protocols::disconnect_mqtt_client))
921 .route("/mqtt/messages/stream", get(protocols::mqtt_messages_stream))
922 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
923 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
924
925 #[cfg(not(feature = "mqtt"))]
926 let router = router
927 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
928 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
929
930 #[cfg(feature = "kafka")]
931 let router = router
932 .route("/kafka/stats", get(protocols::get_kafka_stats))
933 .route("/kafka/topics", get(protocols::get_kafka_topics))
934 .route("/kafka/topics/{topic}", get(protocols::get_kafka_topic))
935 .route("/kafka/groups", get(protocols::get_kafka_groups))
936 .route("/kafka/groups/{group_id}", get(protocols::get_kafka_group))
937 .route("/kafka/produce", post(protocols::produce_kafka_message))
938 .route("/kafka/produce/batch", post(protocols::produce_kafka_batch))
939 .route("/kafka/messages/stream", get(protocols::kafka_messages_stream));
940
941 #[cfg(not(feature = "kafka"))]
942 let router = router;
943
944 let router = router
946 .route("/migration/routes", get(migration::get_migration_routes))
947 .route("/migration/routes/{pattern}/toggle", post(migration::toggle_route_migration))
948 .route("/migration/routes/{pattern}", put(migration::set_route_migration_mode))
949 .route("/migration/groups/{group}/toggle", post(migration::toggle_group_migration))
950 .route("/migration/groups/{group}", put(migration::set_group_migration_mode))
951 .route("/migration/groups", get(migration::get_migration_groups))
952 .route("/migration/status", get(migration::get_migration_status));
953
954 let router = router
956 .route("/proxy/rules", get(proxy::list_proxy_rules))
957 .route("/proxy/rules", post(proxy::create_proxy_rule))
958 .route("/proxy/rules/{id}", get(proxy::get_proxy_rule))
959 .route("/proxy/rules/{id}", put(proxy::update_proxy_rule))
960 .route("/proxy/rules/{id}", delete(proxy::delete_proxy_rule))
961 .route("/proxy/inspect", get(proxy::get_proxy_inspect));
962
963 let router = router.route("/ai/generate-spec", post(generate_ai_spec));
965
966 let router = router.nest(
968 "/snapshot-diff",
969 crate::handlers::snapshot_diff::snapshot_diff_router(state.clone()),
970 );
971
972 #[cfg(feature = "behavioral-cloning")]
973 let router = router.route("/mockai/generate-openapi", post(generate_openapi_from_traffic));
974
975 let router = router
976 .route("/mockai/learn", post(learn_from_examples))
977 .route("/mockai/rules/explanations", get(list_rule_explanations))
978 .route("/mockai/rules/{id}/explanation", get(get_rule_explanation))
979 .route("/chaos/config", get(get_chaos_config))
980 .route("/chaos/config", post(update_chaos_config))
981 .route("/network/profiles", get(list_network_profiles))
982 .route("/network/profile/apply", post(apply_network_profile));
983
984 let router =
986 router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
987
988 #[cfg(feature = "conformance")]
990 let router = router.nest_service(
991 "/conformance",
992 crate::handlers::conformance::conformance_router(state.conformance_state.clone()),
993 );
994 #[cfg(not(feature = "conformance"))]
995 let router = router;
996
997 router.with_state(state)
998}
999
1000pub fn management_router_with_ui_builder(
1002 state: ManagementState,
1003 server_config: mockforge_core::config::ServerConfig,
1004) -> Router {
1005 use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
1006
1007 let management = management_router(state);
1009
1010 let ui_builder_state = UIBuilderState::new(server_config);
1012 let ui_builder = create_ui_builder_router(ui_builder_state);
1013
1014 management.nest("/ui-builder", ui_builder)
1016}
1017
1018pub fn management_router_with_spec_import(state: ManagementState) -> Router {
1020 use crate::spec_import::{spec_import_router, SpecImportState};
1021
1022 let management = management_router(state);
1024
1025 Router::new()
1027 .merge(management)
1028 .merge(spec_import_router(SpecImportState::new()))
1029}
1030
1031pub async fn serve_dynamic_mock(state: &ManagementState, req: Request<Body>) -> Option<Response> {
1041 let method = req.method().as_str().to_string();
1042 let path = req.uri().path().to_string();
1043
1044 let query_params: std::collections::HashMap<String, String> = req
1045 .uri()
1046 .query()
1047 .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
1048 .unwrap_or_default();
1049
1050 let headers: std::collections::HashMap<String, String> = req
1051 .headers()
1052 .iter()
1053 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str().to_string(), v.to_string())))
1054 .collect();
1055
1056 let (_parts, body) = req.into_parts();
1057 let body_bytes = axum::body::to_bytes(body, 1024 * 1024).await.ok()?;
1058 let body_opt: Option<&[u8]> = if body_bytes.is_empty() {
1059 None
1060 } else {
1061 Some(&body_bytes)
1062 };
1063
1064 let mocks = state.mocks.read().await;
1065
1066 let mut candidates: Vec<&MockConfig> = mocks
1067 .iter()
1068 .filter(|m| mock_matches_request(m, &method, &path, &headers, &query_params, body_opt))
1069 .collect();
1070 if candidates.is_empty() {
1071 return None;
1072 }
1073 candidates.sort_by_key(|m| -(m.priority.unwrap_or(0)));
1074 let mock = candidates.first()?;
1075
1076 if let Some(ms) = mock.latency_ms {
1077 if ms > 0 {
1078 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1079 }
1080 }
1081
1082 let status = mock
1083 .status_code
1084 .and_then(|c| StatusCode::from_u16(c).ok())
1085 .unwrap_or(StatusCode::OK);
1086
1087 let template_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1095 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1096 .unwrap_or(false);
1097 let body_value = if template_expand {
1098 let body_clone = mock.response.body.clone();
1099 match tokio::task::spawn_blocking(move || {
1100 mockforge_core::templating::expand_tokens(&body_clone)
1101 })
1102 .await
1103 {
1104 Ok(expanded) => expanded,
1105 Err(_) => mock.response.body.clone(),
1106 }
1107 } else {
1108 mock.response.body.clone()
1109 };
1110
1111 let body_bytes_out = serde_json::to_vec(&body_value).unwrap_or_default();
1112 let mut response = Response::builder().status(status);
1113
1114 let mut has_content_type = false;
1115 if let Some(h) = &mock.response.headers {
1116 for (k, v) in h {
1117 if k.eq_ignore_ascii_case("content-type") {
1118 has_content_type = true;
1119 }
1120 if let (Ok(name), Ok(value)) =
1121 (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(v))
1122 {
1123 response = response.header(name, value);
1124 }
1125 }
1126 }
1127 if !has_content_type {
1128 response = response.header("content-type", "application/json");
1129 }
1130
1131 Some(
1132 response
1133 .body(Body::from(body_bytes_out))
1134 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
1135 )
1136}
1137
1138pub async fn dynamic_mock_fallback(
1145 State(state): State<ManagementState>,
1146 req: Request<Body>,
1147) -> Response {
1148 match serve_dynamic_mock(&state, req).await {
1149 Some(resp) => resp,
1150 None => StatusCode::NOT_FOUND.into_response(),
1151 }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156 use super::*;
1157
1158 #[tokio::test]
1159 async fn test_create_and_get_mock() {
1160 let state = ManagementState::new(None, None, 3000);
1161
1162 let mock = MockConfig {
1163 id: "test-1".to_string(),
1164 name: "Test Mock".to_string(),
1165 method: "GET".to_string(),
1166 path: "/test".to_string(),
1167 response: MockResponse {
1168 body: serde_json::json!({"message": "test"}),
1169 headers: None,
1170 },
1171 enabled: true,
1172 latency_ms: None,
1173 status_code: Some(200),
1174 request_match: None,
1175 priority: None,
1176 scenario: None,
1177 required_scenario_state: None,
1178 new_scenario_state: None,
1179 };
1180
1181 {
1183 let mut mocks = state.mocks.write().await;
1184 mocks.push(mock.clone());
1185 }
1186
1187 let mocks = state.mocks.read().await;
1189 let found = mocks.iter().find(|m| m.id == "test-1");
1190 assert!(found.is_some());
1191 assert_eq!(found.unwrap().name, "Test Mock");
1192 }
1193
1194 #[tokio::test]
1195 async fn test_server_stats() {
1196 let state = ManagementState::new(None, None, 3000);
1197
1198 {
1200 let mut mocks = state.mocks.write().await;
1201 mocks.push(MockConfig {
1202 id: "1".to_string(),
1203 name: "Mock 1".to_string(),
1204 method: "GET".to_string(),
1205 path: "/test1".to_string(),
1206 response: MockResponse {
1207 body: serde_json::json!({}),
1208 headers: None,
1209 },
1210 enabled: true,
1211 latency_ms: None,
1212 status_code: Some(200),
1213 request_match: None,
1214 priority: None,
1215 scenario: None,
1216 required_scenario_state: None,
1217 new_scenario_state: None,
1218 });
1219 mocks.push(MockConfig {
1220 id: "2".to_string(),
1221 name: "Mock 2".to_string(),
1222 method: "POST".to_string(),
1223 path: "/test2".to_string(),
1224 response: MockResponse {
1225 body: serde_json::json!({}),
1226 headers: None,
1227 },
1228 enabled: false,
1229 latency_ms: None,
1230 status_code: Some(201),
1231 request_match: None,
1232 priority: None,
1233 scenario: None,
1234 required_scenario_state: None,
1235 new_scenario_state: None,
1236 });
1237 }
1238
1239 let mocks = state.mocks.read().await;
1240 assert_eq!(mocks.len(), 2);
1241 assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
1242 }
1243
1244 #[test]
1245 fn test_mock_matches_request_with_xpath_absolute_path() {
1246 let mock = MockConfig {
1247 id: "xpath-1".to_string(),
1248 name: "XPath Match".to_string(),
1249 method: "POST".to_string(),
1250 path: "/xml".to_string(),
1251 response: MockResponse {
1252 body: serde_json::json!({"ok": true}),
1253 headers: None,
1254 },
1255 enabled: true,
1256 latency_ms: None,
1257 status_code: Some(200),
1258 request_match: Some(RequestMatchCriteria {
1259 xpath: Some("/root/order/id".to_string()),
1260 ..Default::default()
1261 }),
1262 priority: None,
1263 scenario: None,
1264 required_scenario_state: None,
1265 new_scenario_state: None,
1266 };
1267
1268 let body = br#"<root><order><id>123</id></order></root>"#;
1269 let headers = std::collections::HashMap::new();
1270 let query = std::collections::HashMap::new();
1271
1272 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1273 }
1274
1275 #[test]
1276 fn test_mock_matches_request_with_xpath_text_predicate() {
1277 let mock = MockConfig {
1278 id: "xpath-2".to_string(),
1279 name: "XPath Predicate Match".to_string(),
1280 method: "POST".to_string(),
1281 path: "/xml".to_string(),
1282 response: MockResponse {
1283 body: serde_json::json!({"ok": true}),
1284 headers: None,
1285 },
1286 enabled: true,
1287 latency_ms: None,
1288 status_code: Some(200),
1289 request_match: Some(RequestMatchCriteria {
1290 xpath: Some("//order/id[text()='123']".to_string()),
1291 ..Default::default()
1292 }),
1293 priority: None,
1294 scenario: None,
1295 required_scenario_state: None,
1296 new_scenario_state: None,
1297 };
1298
1299 let body = br#"<root><order><id>123</id></order></root>"#;
1300 let headers = std::collections::HashMap::new();
1301 let query = std::collections::HashMap::new();
1302
1303 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1304 }
1305
1306 #[test]
1307 fn test_mock_matches_request_with_xpath_no_match() {
1308 let mock = MockConfig {
1309 id: "xpath-3".to_string(),
1310 name: "XPath No Match".to_string(),
1311 method: "POST".to_string(),
1312 path: "/xml".to_string(),
1313 response: MockResponse {
1314 body: serde_json::json!({"ok": true}),
1315 headers: None,
1316 },
1317 enabled: true,
1318 latency_ms: None,
1319 status_code: Some(200),
1320 request_match: Some(RequestMatchCriteria {
1321 xpath: Some("//order/id[text()='456']".to_string()),
1322 ..Default::default()
1323 }),
1324 priority: None,
1325 scenario: None,
1326 required_scenario_state: None,
1327 new_scenario_state: None,
1328 };
1329
1330 let body = br#"<root><order><id>123</id></order></root>"#;
1331 let headers = std::collections::HashMap::new();
1332 let query = std::collections::HashMap::new();
1333
1334 assert!(!mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1335 }
1336}