1mod ai_gen;
6mod health;
7mod import_export;
8mod migration;
9mod mocks;
10mod protocols;
11mod proxy;
12
13pub use ai_gen::*;
14pub use health::*;
15pub use import_export::*;
16pub use proxy::{BodyTransformRequest, ProxyRuleRequest, ProxyRuleResponse};
17
18use axum::{
19 body::Body,
20 extract::State,
21 http::{HeaderName, HeaderValue, Request, StatusCode},
22 response::{IntoResponse, Response},
23 routing::{delete, get, post, put},
24 Router,
25};
26use mockforge_openapi::OpenApiSpec;
27use mockforge_proxy::config::ProxyConfig;
28use serde::{Deserialize, Serialize};
29use std::sync::Arc;
30use tokio::sync::{broadcast, RwLock};
31
32#[cfg(any(feature = "mqtt", feature = "kafka"))]
34const DEFAULT_MESSAGE_BROADCAST_CAPACITY: usize = 1000;
35
36#[cfg(any(feature = "mqtt", feature = "kafka"))]
38fn get_message_broadcast_capacity() -> usize {
39 std::env::var("MOCKFORGE_MESSAGE_BROADCAST_CAPACITY")
40 .ok()
41 .and_then(|s| s.parse().ok())
42 .unwrap_or(DEFAULT_MESSAGE_BROADCAST_CAPACITY)
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(tag = "protocol", content = "data")]
48#[serde(rename_all = "lowercase")]
49pub enum MessageEvent {
50 #[cfg(feature = "mqtt")]
51 Mqtt(MqttMessageEvent),
53 #[cfg(feature = "kafka")]
54 Kafka(KafkaMessageEvent),
56}
57
58#[cfg(feature = "mqtt")]
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct MqttMessageEvent {
62 pub topic: String,
64 pub payload: String,
66 pub qos: u8,
68 pub retain: bool,
70 pub timestamp: String,
72}
73
74#[cfg(feature = "kafka")]
75#[allow(missing_docs)]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct KafkaMessageEvent {
78 pub topic: String,
79 pub key: Option<String>,
80 pub value: String,
81 pub partition: i32,
82 pub offset: i64,
83 pub headers: Option<std::collections::HashMap<String, String>>,
84 pub timestamp: String,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct MockConfig {
90 #[serde(default, skip_serializing_if = "String::is_empty")]
94 pub id: String,
95 #[serde(default)]
99 pub name: String,
100 pub method: String,
102 pub path: String,
104 pub response: MockResponse,
106 #[serde(default = "default_true")]
108 pub enabled: bool,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub latency_ms: Option<u64>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub status_code: Option<u16>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub request_match: Option<RequestMatchCriteria>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub priority: Option<i32>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub scenario: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub required_scenario_state: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub new_scenario_state: Option<String>,
130}
131
132fn default_true() -> bool {
133 true
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct MockResponse {
139 pub body: serde_json::Value,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub headers: Option<std::collections::HashMap<String, String>>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148pub struct RequestMatchCriteria {
149 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
151 pub headers: std::collections::HashMap<String, String>,
152 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
154 pub query_params: std::collections::HashMap<String, String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub body_pattern: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub json_path: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub xpath: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub custom_matcher: Option<String>,
167}
168
169pub fn mock_matches_request(
178 mock: &MockConfig,
179 method: &str,
180 path: &str,
181 headers: &std::collections::HashMap<String, String>,
182 query_params: &std::collections::HashMap<String, String>,
183 body: Option<&[u8]>,
184) -> bool {
185 use regex::Regex;
186
187 if !mock.enabled {
189 return false;
190 }
191
192 if mock.method.to_uppercase() != method.to_uppercase() {
194 return false;
195 }
196
197 if !path_matches_pattern(&mock.path, path) {
199 return false;
200 }
201
202 if let Some(criteria) = &mock.request_match {
204 for (key, expected_value) in &criteria.headers {
206 let header_key_lower = key.to_lowercase();
207 let found = headers.iter().find(|(k, _)| k.to_lowercase() == header_key_lower);
208
209 if let Some((_, actual_value)) = found {
210 if let Ok(re) = Regex::new(expected_value) {
212 if !re.is_match(actual_value) {
213 return false;
214 }
215 } else if actual_value != expected_value {
216 return false;
217 }
218 } else {
219 return false; }
221 }
222
223 for (key, expected_value) in &criteria.query_params {
225 if let Some(actual_value) = query_params.get(key) {
226 if actual_value != expected_value {
227 return false;
228 }
229 } else {
230 return false; }
232 }
233
234 if let Some(pattern) = &criteria.body_pattern {
236 if let Some(body_bytes) = body {
237 let body_str = String::from_utf8_lossy(body_bytes);
238 if let Ok(re) = Regex::new(pattern) {
240 if !re.is_match(&body_str) {
241 return false;
242 }
243 } else if body_str.as_ref() != pattern {
244 return false;
245 }
246 } else {
247 return false; }
249 }
250
251 if let Some(json_path) = &criteria.json_path {
253 if let Some(body_bytes) = body {
254 if let Ok(body_str) = std::str::from_utf8(body_bytes) {
255 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(body_str) {
256 if !json_path_exists(&json_value, json_path) {
258 return false;
259 }
260 }
261 }
262 }
263 }
264
265 if let Some(xpath) = &criteria.xpath {
267 if let Some(body_bytes) = body {
268 if let Ok(body_str) = std::str::from_utf8(body_bytes) {
269 if !xml_xpath_exists(body_str, xpath) {
270 return false;
271 }
272 } else {
273 return false;
274 }
275 } else {
276 return false; }
278 }
279
280 if let Some(custom) = &criteria.custom_matcher {
282 if !evaluate_custom_matcher(custom, method, path, headers, query_params, body) {
283 return false;
284 }
285 }
286 }
287
288 true
289}
290
291fn path_matches_pattern(pattern: &str, path: &str) -> bool {
293 if pattern == path {
295 return true;
296 }
297
298 if pattern == "*" {
300 return true;
301 }
302
303 let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
305 let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
306
307 if pattern_parts.len() != path_parts.len() {
308 if pattern.contains('*') {
310 return matches_wildcard_pattern(pattern, path);
311 }
312 return false;
313 }
314
315 for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
316 if pattern_part.starts_with('{') && pattern_part.ends_with('}') {
318 continue; }
320
321 if pattern_part != path_part {
322 return false;
323 }
324 }
325
326 true
327}
328
329fn matches_wildcard_pattern(pattern: &str, path: &str) -> bool {
331 use regex::Regex;
332
333 let regex_pattern = pattern.replace('*', ".*").replace('?', ".?");
335
336 if let Ok(re) = Regex::new(&format!("^{}$", regex_pattern)) {
337 return re.is_match(path);
338 }
339
340 false
341}
342
343fn json_path_exists(json: &serde_json::Value, json_path: &str) -> bool {
351 let path = if json_path == "$" {
352 return true;
353 } else if let Some(p) = json_path.strip_prefix("$.") {
354 p
355 } else if let Some(p) = json_path.strip_prefix('$') {
356 p.strip_prefix('.').unwrap_or(p)
357 } else {
358 json_path
359 };
360
361 let mut current = json;
362 for segment in split_json_path_segments(path) {
363 match segment {
364 JsonPathSegment::Field(name) => {
365 if let Some(obj) = current.as_object() {
366 if let Some(value) = obj.get(name) {
367 current = value;
368 } else {
369 return false;
370 }
371 } else {
372 return false;
373 }
374 }
375 JsonPathSegment::Index(idx) => {
376 if let Some(arr) = current.as_array() {
377 if let Some(value) = arr.get(idx) {
378 current = value;
379 } else {
380 return false;
381 }
382 } else {
383 return false;
384 }
385 }
386 JsonPathSegment::Wildcard => {
387 if let Some(arr) = current.as_array() {
388 return !arr.is_empty();
389 }
390 return false;
391 }
392 }
393 }
394 true
395}
396
397enum JsonPathSegment<'a> {
398 Field(&'a str),
399 Index(usize),
400 Wildcard,
401}
402
403fn split_json_path_segments(path: &str) -> Vec<JsonPathSegment<'_>> {
405 let mut segments = Vec::new();
406 for part in path.split('.') {
407 if part.is_empty() {
408 continue;
409 }
410 if let Some(bracket_start) = part.find('[') {
411 let field_name = &part[..bracket_start];
412 if !field_name.is_empty() {
413 segments.push(JsonPathSegment::Field(field_name));
414 }
415 let bracket_content = &part[bracket_start + 1..part.len() - 1];
416 if bracket_content == "*" {
417 segments.push(JsonPathSegment::Wildcard);
418 } else if let Ok(idx) = bracket_content.parse::<usize>() {
419 segments.push(JsonPathSegment::Index(idx));
420 }
421 } else {
422 segments.push(JsonPathSegment::Field(part));
423 }
424 }
425 segments
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
429struct XPathSegment {
430 name: String,
431 text_equals: Option<String>,
432}
433
434fn parse_xpath_segment(segment: &str) -> Option<XPathSegment> {
435 if segment.is_empty() {
436 return None;
437 }
438
439 let trimmed = segment.trim();
440 if let Some(bracket_start) = trimmed.find('[') {
441 if !trimmed.ends_with(']') {
442 return None;
443 }
444
445 let name = trimmed[..bracket_start].trim();
446 let predicate = &trimmed[bracket_start + 1..trimmed.len() - 1];
447 let predicate = predicate.trim();
448
449 if let Some(raw) = predicate.strip_prefix("text()=") {
451 let raw = raw.trim();
452 if raw.len() >= 2
453 && ((raw.starts_with('"') && raw.ends_with('"'))
454 || (raw.starts_with('\'') && raw.ends_with('\'')))
455 {
456 let text = raw[1..raw.len() - 1].to_string();
457 if !name.is_empty() {
458 return Some(XPathSegment {
459 name: name.to_string(),
460 text_equals: Some(text),
461 });
462 }
463 }
464 }
465
466 None
467 } else {
468 Some(XPathSegment {
469 name: trimmed.to_string(),
470 text_equals: None,
471 })
472 }
473}
474
475fn segment_matches(node: roxmltree::Node<'_, '_>, segment: &XPathSegment) -> bool {
476 if !node.is_element() {
477 return false;
478 }
479 if node.tag_name().name() != segment.name {
480 return false;
481 }
482 match &segment.text_equals {
483 Some(expected) => node.text().map(str::trim).unwrap_or_default() == expected,
484 None => true,
485 }
486}
487
488fn xml_xpath_exists(xml_body: &str, xpath: &str) -> bool {
495 let doc = match roxmltree::Document::parse(xml_body) {
496 Ok(doc) => doc,
497 Err(err) => {
498 tracing::warn!("Failed to parse XML for XPath matching: {}", err);
499 return false;
500 }
501 };
502
503 let expr = xpath.trim();
504 if expr.is_empty() {
505 return false;
506 }
507
508 let (is_descendant, path_str) = if let Some(rest) = expr.strip_prefix("//") {
509 (true, rest)
510 } else if let Some(rest) = expr.strip_prefix('/') {
511 (false, rest)
512 } else {
513 tracing::warn!("Unsupported XPath expression (must start with / or //): {}", expr);
514 return false;
515 };
516
517 let segments: Vec<XPathSegment> = path_str
518 .split('/')
519 .filter(|s| !s.trim().is_empty())
520 .filter_map(parse_xpath_segment)
521 .collect();
522
523 if segments.is_empty() {
524 return false;
525 }
526
527 if is_descendant {
528 let first = &segments[0];
529 for node in doc.descendants().filter(|n| segment_matches(*n, first)) {
530 let mut frontier = vec![node];
531 for segment in &segments[1..] {
532 let mut next_frontier = Vec::new();
533 for parent in &frontier {
534 for child in parent.children().filter(|n| segment_matches(*n, segment)) {
535 next_frontier.push(child);
536 }
537 }
538 if next_frontier.is_empty() {
539 frontier.clear();
540 break;
541 }
542 frontier = next_frontier;
543 }
544 if !frontier.is_empty() {
545 return true;
546 }
547 }
548 false
549 } else {
550 let mut frontier = vec![doc.root_element()];
551 for (index, segment) in segments.iter().enumerate() {
552 let mut next_frontier = Vec::new();
553 for parent in &frontier {
554 if index == 0 {
555 if segment_matches(*parent, segment) {
556 next_frontier.push(*parent);
557 }
558 continue;
559 }
560 for child in parent.children().filter(|n| segment_matches(*n, segment)) {
561 next_frontier.push(child);
562 }
563 }
564 if next_frontier.is_empty() {
565 return false;
566 }
567 frontier = next_frontier;
568 }
569 !frontier.is_empty()
570 }
571}
572
573fn evaluate_custom_matcher(
575 expression: &str,
576 method: &str,
577 path: &str,
578 headers: &std::collections::HashMap<String, String>,
579 query_params: &std::collections::HashMap<String, String>,
580 body: Option<&[u8]>,
581) -> bool {
582 use regex::Regex;
583
584 let expr = expression.trim();
585
586 if expr.contains("==") {
588 let parts: Vec<&str> = expr.split("==").map(|s| s.trim()).collect();
589 if parts.len() != 2 {
590 return false;
591 }
592
593 let field = parts[0];
594 let expected_value = parts[1].trim_matches('"').trim_matches('\'');
595
596 match field {
597 "method" => method == expected_value,
598 "path" => path == expected_value,
599 _ if field.starts_with("headers.") => {
600 let header_name = &field[8..];
601 headers.get(header_name).map(|v| v == expected_value).unwrap_or(false)
602 }
603 _ if field.starts_with("query.") => {
604 let param_name = &field[6..];
605 query_params.get(param_name).map(|v| v == expected_value).unwrap_or(false)
606 }
607 _ => false,
608 }
609 }
610 else if expr.contains("=~") {
612 let parts: Vec<&str> = expr.split("=~").map(|s| s.trim()).collect();
613 if parts.len() != 2 {
614 return false;
615 }
616
617 let field = parts[0];
618 let pattern = parts[1].trim_matches('"').trim_matches('\'');
619
620 if let Ok(re) = Regex::new(pattern) {
621 match field {
622 "method" => re.is_match(method),
623 "path" => re.is_match(path),
624 _ if field.starts_with("headers.") => {
625 let header_name = &field[8..];
626 headers.get(header_name).map(|v| re.is_match(v)).unwrap_or(false)
627 }
628 _ if field.starts_with("query.") => {
629 let param_name = &field[6..];
630 query_params.get(param_name).map(|v| re.is_match(v)).unwrap_or(false)
631 }
632 _ => false,
633 }
634 } else {
635 false
636 }
637 }
638 else if expr.contains("contains") {
640 let parts: Vec<&str> = expr.split("contains").map(|s| s.trim()).collect();
641 if parts.len() != 2 {
642 return false;
643 }
644
645 let field = parts[0];
646 let search_value = parts[1].trim_matches('"').trim_matches('\'');
647
648 match field {
649 "path" => path.contains(search_value),
650 _ if field.starts_with("headers.") => {
651 let header_name = &field[8..];
652 headers.get(header_name).map(|v| v.contains(search_value)).unwrap_or(false)
653 }
654 _ if field.starts_with("body") => {
655 if let Some(body_bytes) = body {
656 let body_str = String::from_utf8_lossy(body_bytes);
657 body_str.contains(search_value)
658 } else {
659 false
660 }
661 }
662 _ => false,
663 }
664 } else {
665 tracing::warn!("Unknown custom matcher expression format: {}", expr);
667 false
668 }
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct ServerStats {
674 pub uptime_seconds: u64,
676 pub total_requests: u64,
678 pub active_mocks: usize,
680 pub enabled_mocks: usize,
682 pub registered_routes: usize,
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct ServerConfig {
689 pub version: String,
691 pub port: u16,
693 pub has_openapi_spec: bool,
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub spec_path: Option<String>,
698}
699
700#[derive(Clone)]
702pub struct ManagementState {
703 pub mocks: Arc<RwLock<Vec<MockConfig>>>,
705 pub spec: Option<Arc<OpenApiSpec>>,
707 pub spec_path: Option<String>,
709 pub port: u16,
711 pub start_time: std::time::Instant,
713 pub request_counter: Arc<RwLock<u64>>,
715 pub proxy_config: Option<Arc<RwLock<ProxyConfig>>>,
717 #[cfg(feature = "smtp")]
719 pub smtp_registry: Option<Arc<mockforge_smtp::SmtpSpecRegistry>>,
720 #[cfg(feature = "mqtt")]
722 pub mqtt_broker: Option<Arc<mockforge_mqtt::MqttBroker>>,
723 #[cfg(feature = "kafka")]
725 pub kafka_broker: Option<Arc<mockforge_kafka::KafkaMockBroker>>,
726 #[cfg(any(feature = "mqtt", feature = "kafka"))]
728 pub message_events: Arc<broadcast::Sender<MessageEvent>>,
729 pub state_machine_manager:
731 Arc<RwLock<mockforge_scenarios::state_machine::ScenarioStateMachineManager>>,
732 pub ws_broadcast: Option<Arc<broadcast::Sender<crate::management_ws::MockEvent>>>,
734 pub lifecycle_hooks: Option<Arc<mockforge_core::lifecycle::LifecycleHookRegistry>>,
736 pub rule_explanations: Arc<
738 RwLock<
739 std::collections::HashMap<
740 String,
741 mockforge_foundation::intelligent_behavior::rule_types::RuleExplanation,
742 >,
743 >,
744 >,
745 #[cfg(feature = "chaos")]
747 pub chaos_api_state: Option<Arc<mockforge_chaos::api::ChaosApiState>>,
748 pub server_config: Option<Arc<RwLock<mockforge_core::config::ServerConfig>>>,
750 #[cfg(feature = "conformance")]
752 pub conformance_state: crate::handlers::conformance::ConformanceState,
753}
754
755impl ManagementState {
756 pub fn new(spec: Option<Arc<OpenApiSpec>>, spec_path: Option<String>, port: u16) -> Self {
763 Self {
764 mocks: Arc::new(RwLock::new(Vec::new())),
765 spec,
766 spec_path,
767 port,
768 start_time: std::time::Instant::now(),
769 request_counter: Arc::new(RwLock::new(0)),
770 proxy_config: None,
771 #[cfg(feature = "smtp")]
772 smtp_registry: None,
773 #[cfg(feature = "mqtt")]
774 mqtt_broker: None,
775 #[cfg(feature = "kafka")]
776 kafka_broker: None,
777 #[cfg(any(feature = "mqtt", feature = "kafka"))]
778 message_events: {
779 let capacity = get_message_broadcast_capacity();
780 let (tx, _) = broadcast::channel(capacity);
781 Arc::new(tx)
782 },
783 state_machine_manager: Arc::new(RwLock::new(
784 mockforge_scenarios::state_machine::ScenarioStateMachineManager::new(),
785 )),
786 ws_broadcast: None,
787 lifecycle_hooks: None,
788 rule_explanations: Arc::new(RwLock::new(std::collections::HashMap::new())),
789 #[cfg(feature = "chaos")]
790 chaos_api_state: None,
791 server_config: None,
792 #[cfg(feature = "conformance")]
793 conformance_state: crate::handlers::conformance::ConformanceState::new(),
794 }
795 }
796
797 pub fn with_lifecycle_hooks(
799 mut self,
800 hooks: Arc<mockforge_core::lifecycle::LifecycleHookRegistry>,
801 ) -> Self {
802 self.lifecycle_hooks = Some(hooks);
803 self
804 }
805
806 pub fn with_ws_broadcast(
808 mut self,
809 ws_broadcast: Arc<broadcast::Sender<crate::management_ws::MockEvent>>,
810 ) -> Self {
811 self.ws_broadcast = Some(ws_broadcast);
812 self
813 }
814
815 pub fn with_proxy_config(mut self, proxy_config: Arc<RwLock<ProxyConfig>>) -> Self {
817 self.proxy_config = Some(proxy_config);
818 self
819 }
820
821 #[cfg(feature = "smtp")]
822 pub fn with_smtp_registry(
824 mut self,
825 smtp_registry: Arc<mockforge_smtp::SmtpSpecRegistry>,
826 ) -> Self {
827 self.smtp_registry = Some(smtp_registry);
828 self
829 }
830
831 #[cfg(feature = "mqtt")]
832 pub fn with_mqtt_broker(mut self, mqtt_broker: Arc<mockforge_mqtt::MqttBroker>) -> Self {
834 self.mqtt_broker = Some(mqtt_broker);
835 self
836 }
837
838 #[cfg(feature = "kafka")]
839 pub fn with_kafka_broker(
841 mut self,
842 kafka_broker: Arc<mockforge_kafka::KafkaMockBroker>,
843 ) -> Self {
844 self.kafka_broker = Some(kafka_broker);
845 self
846 }
847
848 #[cfg(feature = "chaos")]
849 pub fn with_chaos_api_state(
851 mut self,
852 chaos_api_state: Arc<mockforge_chaos::api::ChaosApiState>,
853 ) -> Self {
854 self.chaos_api_state = Some(chaos_api_state);
855 self
856 }
857
858 pub fn with_server_config(
860 mut self,
861 server_config: Arc<RwLock<mockforge_core::config::ServerConfig>>,
862 ) -> Self {
863 self.server_config = Some(server_config);
864 self
865 }
866}
867
868pub fn management_router(state: ManagementState) -> Router {
870 let router = Router::new()
871 .route("/capabilities", get(get_capabilities))
872 .route("/health", get(health_check))
873 .route("/stats", get(get_stats))
874 .route("/config", get(get_config))
875 .route("/config/validate", post(validate_config))
876 .route("/config/bulk", post(bulk_update_config))
877 .route("/mocks", get(mocks::list_mocks))
878 .route("/mocks", post(mocks::create_mock))
879 .route("/mocks/{id}", get(mocks::get_mock))
880 .route("/mocks/{id}", put(mocks::update_mock))
881 .route("/mocks/{id}", delete(mocks::delete_mock))
882 .route("/export", get(export_mocks))
883 .route("/import", post(import_mocks))
884 .route("/spec", get(get_openapi_spec));
885
886 #[cfg(feature = "smtp")]
887 let router = router
888 .route("/smtp/mailbox", get(protocols::list_smtp_emails))
889 .route("/smtp/mailbox", delete(protocols::clear_smtp_mailbox))
890 .route("/smtp/mailbox/{id}", get(protocols::get_smtp_email))
891 .route("/smtp/mailbox/export", get(protocols::export_smtp_mailbox))
892 .route("/smtp/mailbox/search", get(protocols::search_smtp_emails));
893
894 #[cfg(not(feature = "smtp"))]
895 let router = router;
896
897 #[cfg(feature = "mqtt")]
899 let router = router
900 .route("/mqtt/stats", get(protocols::get_mqtt_stats))
901 .route("/mqtt/clients", get(protocols::get_mqtt_clients))
902 .route("/mqtt/topics", get(protocols::get_mqtt_topics))
903 .route("/mqtt/clients/{client_id}", delete(protocols::disconnect_mqtt_client))
904 .route("/mqtt/messages/stream", get(protocols::mqtt_messages_stream))
905 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
906 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
907
908 #[cfg(not(feature = "mqtt"))]
909 let router = router
910 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
911 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
912
913 #[cfg(feature = "kafka")]
914 let router = router
915 .route("/kafka/stats", get(protocols::get_kafka_stats))
916 .route("/kafka/topics", get(protocols::get_kafka_topics))
917 .route("/kafka/topics/{topic}", get(protocols::get_kafka_topic))
918 .route("/kafka/groups", get(protocols::get_kafka_groups))
919 .route("/kafka/groups/{group_id}", get(protocols::get_kafka_group))
920 .route("/kafka/produce", post(protocols::produce_kafka_message))
921 .route("/kafka/produce/batch", post(protocols::produce_kafka_batch))
922 .route("/kafka/messages/stream", get(protocols::kafka_messages_stream));
923
924 #[cfg(not(feature = "kafka"))]
925 let router = router;
926
927 let router = router
929 .route("/migration/routes", get(migration::get_migration_routes))
930 .route("/migration/routes/{pattern}/toggle", post(migration::toggle_route_migration))
931 .route("/migration/routes/{pattern}", put(migration::set_route_migration_mode))
932 .route("/migration/groups/{group}/toggle", post(migration::toggle_group_migration))
933 .route("/migration/groups/{group}", put(migration::set_group_migration_mode))
934 .route("/migration/groups", get(migration::get_migration_groups))
935 .route("/migration/status", get(migration::get_migration_status));
936
937 let router = router
939 .route("/proxy/rules", get(proxy::list_proxy_rules))
940 .route("/proxy/rules", post(proxy::create_proxy_rule))
941 .route("/proxy/rules/{id}", get(proxy::get_proxy_rule))
942 .route("/proxy/rules/{id}", put(proxy::update_proxy_rule))
943 .route("/proxy/rules/{id}", delete(proxy::delete_proxy_rule))
944 .route("/proxy/inspect", get(proxy::get_proxy_inspect));
945
946 let router = router.route("/ai/generate-spec", post(generate_ai_spec));
948
949 let router = router.nest(
951 "/snapshot-diff",
952 crate::handlers::snapshot_diff::snapshot_diff_router(state.clone()),
953 );
954
955 #[cfg(feature = "behavioral-cloning")]
956 let router = router.route("/mockai/generate-openapi", post(generate_openapi_from_traffic));
957
958 let router = router
959 .route("/mockai/learn", post(learn_from_examples))
960 .route("/mockai/rules/explanations", get(list_rule_explanations))
961 .route("/mockai/rules/{id}/explanation", get(get_rule_explanation))
962 .route("/chaos/config", get(get_chaos_config))
963 .route("/chaos/config", post(update_chaos_config))
964 .route("/network/profiles", get(list_network_profiles))
965 .route("/network/profile/apply", post(apply_network_profile));
966
967 let router =
969 router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
970
971 #[cfg(feature = "conformance")]
973 let router = router.nest_service(
974 "/conformance",
975 crate::handlers::conformance::conformance_router(state.conformance_state.clone()),
976 );
977 #[cfg(not(feature = "conformance"))]
978 let router = router;
979
980 router.with_state(state)
981}
982
983pub fn management_router_with_ui_builder(
985 state: ManagementState,
986 server_config: mockforge_core::config::ServerConfig,
987) -> Router {
988 use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
989
990 let management = management_router(state);
992
993 let ui_builder_state = UIBuilderState::new(server_config);
995 let ui_builder = create_ui_builder_router(ui_builder_state);
996
997 management.nest("/ui-builder", ui_builder)
999}
1000
1001pub fn management_router_with_spec_import(state: ManagementState) -> Router {
1003 use crate::spec_import::{spec_import_router, SpecImportState};
1004
1005 let management = management_router(state);
1007
1008 Router::new()
1010 .merge(management)
1011 .merge(spec_import_router(SpecImportState::new()))
1012}
1013
1014pub async fn serve_dynamic_mock(state: &ManagementState, req: Request<Body>) -> Option<Response> {
1024 let method = req.method().as_str().to_string();
1025 let path = req.uri().path().to_string();
1026
1027 let query_params: std::collections::HashMap<String, String> = req
1028 .uri()
1029 .query()
1030 .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
1031 .unwrap_or_default();
1032
1033 let headers: std::collections::HashMap<String, String> = req
1034 .headers()
1035 .iter()
1036 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str().to_string(), v.to_string())))
1037 .collect();
1038
1039 let (_parts, body) = req.into_parts();
1040 let body_bytes = axum::body::to_bytes(body, 1024 * 1024).await.ok()?;
1041 let body_opt: Option<&[u8]> = if body_bytes.is_empty() {
1042 None
1043 } else {
1044 Some(&body_bytes)
1045 };
1046
1047 let mocks = state.mocks.read().await;
1048
1049 let mut candidates: Vec<&MockConfig> = mocks
1050 .iter()
1051 .filter(|m| mock_matches_request(m, &method, &path, &headers, &query_params, body_opt))
1052 .collect();
1053 if candidates.is_empty() {
1054 return None;
1055 }
1056 candidates.sort_by_key(|m| -(m.priority.unwrap_or(0)));
1057 let mock = candidates.first()?;
1058
1059 if let Some(ms) = mock.latency_ms {
1060 if ms > 0 {
1061 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1062 }
1063 }
1064
1065 let status = mock
1066 .status_code
1067 .and_then(|c| StatusCode::from_u16(c).ok())
1068 .unwrap_or(StatusCode::OK);
1069
1070 let template_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1078 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1079 .unwrap_or(false);
1080 let body_value = if template_expand {
1081 let body_clone = mock.response.body.clone();
1082 match tokio::task::spawn_blocking(move || {
1083 mockforge_core::templating::expand_tokens(&body_clone)
1084 })
1085 .await
1086 {
1087 Ok(expanded) => expanded,
1088 Err(_) => mock.response.body.clone(),
1089 }
1090 } else {
1091 mock.response.body.clone()
1092 };
1093
1094 let body_bytes_out = serde_json::to_vec(&body_value).unwrap_or_default();
1095 let mut response = Response::builder().status(status);
1096
1097 let mut has_content_type = false;
1098 if let Some(h) = &mock.response.headers {
1099 for (k, v) in h {
1100 if k.eq_ignore_ascii_case("content-type") {
1101 has_content_type = true;
1102 }
1103 if let (Ok(name), Ok(value)) =
1104 (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(v))
1105 {
1106 response = response.header(name, value);
1107 }
1108 }
1109 }
1110 if !has_content_type {
1111 response = response.header("content-type", "application/json");
1112 }
1113
1114 Some(
1115 response
1116 .body(Body::from(body_bytes_out))
1117 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
1118 )
1119}
1120
1121pub async fn dynamic_mock_fallback(
1128 State(state): State<ManagementState>,
1129 req: Request<Body>,
1130) -> Response {
1131 match serve_dynamic_mock(&state, req).await {
1132 Some(resp) => resp,
1133 None => StatusCode::NOT_FOUND.into_response(),
1134 }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140
1141 #[tokio::test]
1142 async fn test_create_and_get_mock() {
1143 let state = ManagementState::new(None, None, 3000);
1144
1145 let mock = MockConfig {
1146 id: "test-1".to_string(),
1147 name: "Test Mock".to_string(),
1148 method: "GET".to_string(),
1149 path: "/test".to_string(),
1150 response: MockResponse {
1151 body: serde_json::json!({"message": "test"}),
1152 headers: None,
1153 },
1154 enabled: true,
1155 latency_ms: None,
1156 status_code: Some(200),
1157 request_match: None,
1158 priority: None,
1159 scenario: None,
1160 required_scenario_state: None,
1161 new_scenario_state: None,
1162 };
1163
1164 {
1166 let mut mocks = state.mocks.write().await;
1167 mocks.push(mock.clone());
1168 }
1169
1170 let mocks = state.mocks.read().await;
1172 let found = mocks.iter().find(|m| m.id == "test-1");
1173 assert!(found.is_some());
1174 assert_eq!(found.unwrap().name, "Test Mock");
1175 }
1176
1177 #[tokio::test]
1178 async fn test_server_stats() {
1179 let state = ManagementState::new(None, None, 3000);
1180
1181 {
1183 let mut mocks = state.mocks.write().await;
1184 mocks.push(MockConfig {
1185 id: "1".to_string(),
1186 name: "Mock 1".to_string(),
1187 method: "GET".to_string(),
1188 path: "/test1".to_string(),
1189 response: MockResponse {
1190 body: serde_json::json!({}),
1191 headers: None,
1192 },
1193 enabled: true,
1194 latency_ms: None,
1195 status_code: Some(200),
1196 request_match: None,
1197 priority: None,
1198 scenario: None,
1199 required_scenario_state: None,
1200 new_scenario_state: None,
1201 });
1202 mocks.push(MockConfig {
1203 id: "2".to_string(),
1204 name: "Mock 2".to_string(),
1205 method: "POST".to_string(),
1206 path: "/test2".to_string(),
1207 response: MockResponse {
1208 body: serde_json::json!({}),
1209 headers: None,
1210 },
1211 enabled: false,
1212 latency_ms: None,
1213 status_code: Some(201),
1214 request_match: None,
1215 priority: None,
1216 scenario: None,
1217 required_scenario_state: None,
1218 new_scenario_state: None,
1219 });
1220 }
1221
1222 let mocks = state.mocks.read().await;
1223 assert_eq!(mocks.len(), 2);
1224 assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
1225 }
1226
1227 #[test]
1228 fn test_mock_matches_request_with_xpath_absolute_path() {
1229 let mock = MockConfig {
1230 id: "xpath-1".to_string(),
1231 name: "XPath Match".to_string(),
1232 method: "POST".to_string(),
1233 path: "/xml".to_string(),
1234 response: MockResponse {
1235 body: serde_json::json!({"ok": true}),
1236 headers: None,
1237 },
1238 enabled: true,
1239 latency_ms: None,
1240 status_code: Some(200),
1241 request_match: Some(RequestMatchCriteria {
1242 xpath: Some("/root/order/id".to_string()),
1243 ..Default::default()
1244 }),
1245 priority: None,
1246 scenario: None,
1247 required_scenario_state: None,
1248 new_scenario_state: None,
1249 };
1250
1251 let body = br#"<root><order><id>123</id></order></root>"#;
1252 let headers = std::collections::HashMap::new();
1253 let query = std::collections::HashMap::new();
1254
1255 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1256 }
1257
1258 #[test]
1259 fn test_mock_matches_request_with_xpath_text_predicate() {
1260 let mock = MockConfig {
1261 id: "xpath-2".to_string(),
1262 name: "XPath Predicate Match".to_string(),
1263 method: "POST".to_string(),
1264 path: "/xml".to_string(),
1265 response: MockResponse {
1266 body: serde_json::json!({"ok": true}),
1267 headers: None,
1268 },
1269 enabled: true,
1270 latency_ms: None,
1271 status_code: Some(200),
1272 request_match: Some(RequestMatchCriteria {
1273 xpath: Some("//order/id[text()='123']".to_string()),
1274 ..Default::default()
1275 }),
1276 priority: None,
1277 scenario: None,
1278 required_scenario_state: None,
1279 new_scenario_state: None,
1280 };
1281
1282 let body = br#"<root><order><id>123</id></order></root>"#;
1283 let headers = std::collections::HashMap::new();
1284 let query = std::collections::HashMap::new();
1285
1286 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1287 }
1288
1289 #[test]
1290 fn test_mock_matches_request_with_xpath_no_match() {
1291 let mock = MockConfig {
1292 id: "xpath-3".to_string(),
1293 name: "XPath No Match".to_string(),
1294 method: "POST".to_string(),
1295 path: "/xml".to_string(),
1296 response: MockResponse {
1297 body: serde_json::json!({"ok": true}),
1298 headers: None,
1299 },
1300 enabled: true,
1301 latency_ms: None,
1302 status_code: Some(200),
1303 request_match: Some(RequestMatchCriteria {
1304 xpath: Some("//order/id[text()='456']".to_string()),
1305 ..Default::default()
1306 }),
1307 priority: None,
1308 scenario: None,
1309 required_scenario_state: None,
1310 new_scenario_state: None,
1311 };
1312
1313 let body = br#"<root><order><id>123</id></order></root>"#;
1314 let headers = std::collections::HashMap::new();
1315 let query = std::collections::HashMap::new();
1316
1317 assert!(!mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1318 }
1319}