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(health::get_capabilities))
872 .route("/health", get(health::health_check))
873 .route("/stats", get(health::get_stats))
874 .route("/config", get(health::get_config))
875 .route("/config/validate", post(health::validate_config))
876 .route("/config/bulk", post(health::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(import_export::export_mocks))
883 .route("/import", post(import_export::import_mocks))
884 .route("/spec", get(health::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(ai_gen::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 =
957 router.route("/mockai/generate-openapi", post(ai_gen::generate_openapi_from_traffic));
958
959 let router = router
960 .route("/mockai/learn", post(ai_gen::learn_from_examples))
961 .route("/mockai/rules/explanations", get(ai_gen::list_rule_explanations))
962 .route("/mockai/rules/{id}/explanation", get(ai_gen::get_rule_explanation))
963 .route("/chaos/config", get(ai_gen::get_chaos_config))
964 .route("/chaos/config", post(ai_gen::update_chaos_config))
965 .route("/network/profiles", get(ai_gen::list_network_profiles))
966 .route("/network/profile/apply", post(ai_gen::apply_network_profile));
967
968 let router =
970 router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
971
972 #[cfg(feature = "conformance")]
974 let router = router.nest_service(
975 "/conformance",
976 crate::handlers::conformance::conformance_router(state.conformance_state.clone()),
977 );
978 #[cfg(not(feature = "conformance"))]
979 let router = router;
980
981 router.with_state(state)
982}
983
984pub fn management_router_with_ui_builder(
986 state: ManagementState,
987 server_config: mockforge_core::config::ServerConfig,
988) -> Router {
989 use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
990
991 let management = management_router(state);
993
994 let ui_builder_state = UIBuilderState::new(server_config);
996 let ui_builder = create_ui_builder_router(ui_builder_state);
997
998 management.nest("/ui-builder", ui_builder)
1000}
1001
1002pub fn management_router_with_spec_import(state: ManagementState) -> Router {
1004 use crate::spec_import::{spec_import_router, SpecImportState};
1005
1006 let management = management_router(state);
1008
1009 Router::new()
1011 .merge(management)
1012 .merge(spec_import_router(SpecImportState::new()))
1013}
1014
1015pub async fn serve_dynamic_mock(state: &ManagementState, req: Request<Body>) -> Option<Response> {
1025 let method = req.method().as_str().to_string();
1026 let path = req.uri().path().to_string();
1027
1028 let query_params: std::collections::HashMap<String, String> = req
1029 .uri()
1030 .query()
1031 .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
1032 .unwrap_or_default();
1033
1034 let headers: std::collections::HashMap<String, String> = req
1035 .headers()
1036 .iter()
1037 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str().to_string(), v.to_string())))
1038 .collect();
1039
1040 let (_parts, body) = req.into_parts();
1041 let body_bytes = axum::body::to_bytes(body, 1024 * 1024).await.ok()?;
1042 let body_opt: Option<&[u8]> = if body_bytes.is_empty() {
1043 None
1044 } else {
1045 Some(&body_bytes)
1046 };
1047
1048 let mocks = state.mocks.read().await;
1049
1050 let mut candidates: Vec<&MockConfig> = mocks
1051 .iter()
1052 .filter(|m| mock_matches_request(m, &method, &path, &headers, &query_params, body_opt))
1053 .collect();
1054 if candidates.is_empty() {
1055 return None;
1056 }
1057 candidates.sort_by_key(|m| -(m.priority.unwrap_or(0)));
1058 let mock = candidates.first()?;
1059
1060 if let Some(ms) = mock.latency_ms {
1061 if ms > 0 {
1062 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1063 }
1064 }
1065
1066 let status = mock
1067 .status_code
1068 .and_then(|c| StatusCode::from_u16(c).ok())
1069 .unwrap_or(StatusCode::OK);
1070
1071 let body_bytes_out = serde_json::to_vec(&mock.response.body).unwrap_or_default();
1072 let mut response = Response::builder().status(status);
1073
1074 let mut has_content_type = false;
1075 if let Some(h) = &mock.response.headers {
1076 for (k, v) in h {
1077 if k.eq_ignore_ascii_case("content-type") {
1078 has_content_type = true;
1079 }
1080 if let (Ok(name), Ok(value)) =
1081 (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(v))
1082 {
1083 response = response.header(name, value);
1084 }
1085 }
1086 }
1087 if !has_content_type {
1088 response = response.header("content-type", "application/json");
1089 }
1090
1091 Some(
1092 response
1093 .body(Body::from(body_bytes_out))
1094 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
1095 )
1096}
1097
1098pub async fn dynamic_mock_fallback(
1105 State(state): State<ManagementState>,
1106 req: Request<Body>,
1107) -> Response {
1108 match serve_dynamic_mock(&state, req).await {
1109 Some(resp) => resp,
1110 None => StatusCode::NOT_FOUND.into_response(),
1111 }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117
1118 #[tokio::test]
1119 async fn test_create_and_get_mock() {
1120 let state = ManagementState::new(None, None, 3000);
1121
1122 let mock = MockConfig {
1123 id: "test-1".to_string(),
1124 name: "Test Mock".to_string(),
1125 method: "GET".to_string(),
1126 path: "/test".to_string(),
1127 response: MockResponse {
1128 body: serde_json::json!({"message": "test"}),
1129 headers: None,
1130 },
1131 enabled: true,
1132 latency_ms: None,
1133 status_code: Some(200),
1134 request_match: None,
1135 priority: None,
1136 scenario: None,
1137 required_scenario_state: None,
1138 new_scenario_state: None,
1139 };
1140
1141 {
1143 let mut mocks = state.mocks.write().await;
1144 mocks.push(mock.clone());
1145 }
1146
1147 let mocks = state.mocks.read().await;
1149 let found = mocks.iter().find(|m| m.id == "test-1");
1150 assert!(found.is_some());
1151 assert_eq!(found.unwrap().name, "Test Mock");
1152 }
1153
1154 #[tokio::test]
1155 async fn test_server_stats() {
1156 let state = ManagementState::new(None, None, 3000);
1157
1158 {
1160 let mut mocks = state.mocks.write().await;
1161 mocks.push(MockConfig {
1162 id: "1".to_string(),
1163 name: "Mock 1".to_string(),
1164 method: "GET".to_string(),
1165 path: "/test1".to_string(),
1166 response: MockResponse {
1167 body: serde_json::json!({}),
1168 headers: None,
1169 },
1170 enabled: true,
1171 latency_ms: None,
1172 status_code: Some(200),
1173 request_match: None,
1174 priority: None,
1175 scenario: None,
1176 required_scenario_state: None,
1177 new_scenario_state: None,
1178 });
1179 mocks.push(MockConfig {
1180 id: "2".to_string(),
1181 name: "Mock 2".to_string(),
1182 method: "POST".to_string(),
1183 path: "/test2".to_string(),
1184 response: MockResponse {
1185 body: serde_json::json!({}),
1186 headers: None,
1187 },
1188 enabled: false,
1189 latency_ms: None,
1190 status_code: Some(201),
1191 request_match: None,
1192 priority: None,
1193 scenario: None,
1194 required_scenario_state: None,
1195 new_scenario_state: None,
1196 });
1197 }
1198
1199 let mocks = state.mocks.read().await;
1200 assert_eq!(mocks.len(), 2);
1201 assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
1202 }
1203
1204 #[test]
1205 fn test_mock_matches_request_with_xpath_absolute_path() {
1206 let mock = MockConfig {
1207 id: "xpath-1".to_string(),
1208 name: "XPath Match".to_string(),
1209 method: "POST".to_string(),
1210 path: "/xml".to_string(),
1211 response: MockResponse {
1212 body: serde_json::json!({"ok": true}),
1213 headers: None,
1214 },
1215 enabled: true,
1216 latency_ms: None,
1217 status_code: Some(200),
1218 request_match: Some(RequestMatchCriteria {
1219 xpath: Some("/root/order/id".to_string()),
1220 ..Default::default()
1221 }),
1222 priority: None,
1223 scenario: None,
1224 required_scenario_state: None,
1225 new_scenario_state: None,
1226 };
1227
1228 let body = br#"<root><order><id>123</id></order></root>"#;
1229 let headers = std::collections::HashMap::new();
1230 let query = std::collections::HashMap::new();
1231
1232 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1233 }
1234
1235 #[test]
1236 fn test_mock_matches_request_with_xpath_text_predicate() {
1237 let mock = MockConfig {
1238 id: "xpath-2".to_string(),
1239 name: "XPath Predicate Match".to_string(),
1240 method: "POST".to_string(),
1241 path: "/xml".to_string(),
1242 response: MockResponse {
1243 body: serde_json::json!({"ok": true}),
1244 headers: None,
1245 },
1246 enabled: true,
1247 latency_ms: None,
1248 status_code: Some(200),
1249 request_match: Some(RequestMatchCriteria {
1250 xpath: Some("//order/id[text()='123']".to_string()),
1251 ..Default::default()
1252 }),
1253 priority: None,
1254 scenario: None,
1255 required_scenario_state: None,
1256 new_scenario_state: None,
1257 };
1258
1259 let body = br#"<root><order><id>123</id></order></root>"#;
1260 let headers = std::collections::HashMap::new();
1261 let query = std::collections::HashMap::new();
1262
1263 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1264 }
1265
1266 #[test]
1267 fn test_mock_matches_request_with_xpath_no_match() {
1268 let mock = MockConfig {
1269 id: "xpath-3".to_string(),
1270 name: "XPath No Match".to_string(),
1271 method: "POST".to_string(),
1272 path: "/xml".to_string(),
1273 response: MockResponse {
1274 body: serde_json::json!({"ok": true}),
1275 headers: None,
1276 },
1277 enabled: true,
1278 latency_ms: None,
1279 status_code: Some(200),
1280 request_match: Some(RequestMatchCriteria {
1281 xpath: Some("//order/id[text()='456']".to_string()),
1282 ..Default::default()
1283 }),
1284 priority: None,
1285 scenario: None,
1286 required_scenario_state: None,
1287 new_scenario_state: None,
1288 };
1289
1290 let body = br#"<root><order><id>123</id></order></root>"#;
1291 let headers = std::collections::HashMap::new();
1292 let query = std::collections::HashMap::new();
1293
1294 assert!(!mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1295 }
1296}