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 routing::{delete, get, post, put},
20 Router,
21};
22use mockforge_core::openapi::OpenApiSpec;
23use mockforge_core::proxy::config::ProxyConfig;
24use serde::{Deserialize, Serialize};
25use std::sync::Arc;
26use tokio::sync::{broadcast, RwLock};
27
28#[cfg(any(feature = "mqtt", feature = "kafka"))]
30const DEFAULT_MESSAGE_BROADCAST_CAPACITY: usize = 1000;
31
32#[cfg(any(feature = "mqtt", feature = "kafka"))]
34fn get_message_broadcast_capacity() -> usize {
35 std::env::var("MOCKFORGE_MESSAGE_BROADCAST_CAPACITY")
36 .ok()
37 .and_then(|s| s.parse().ok())
38 .unwrap_or(DEFAULT_MESSAGE_BROADCAST_CAPACITY)
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(tag = "protocol", content = "data")]
44#[serde(rename_all = "lowercase")]
45pub enum MessageEvent {
46 #[cfg(feature = "mqtt")]
47 Mqtt(MqttMessageEvent),
49 #[cfg(feature = "kafka")]
50 Kafka(KafkaMessageEvent),
52}
53
54#[cfg(feature = "mqtt")]
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct MqttMessageEvent {
58 pub topic: String,
60 pub payload: String,
62 pub qos: u8,
64 pub retain: bool,
66 pub timestamp: String,
68}
69
70#[cfg(feature = "kafka")]
71#[allow(missing_docs)]
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct KafkaMessageEvent {
74 pub topic: String,
75 pub key: Option<String>,
76 pub value: String,
77 pub partition: i32,
78 pub offset: i64,
79 pub headers: Option<std::collections::HashMap<String, String>>,
80 pub timestamp: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct MockConfig {
86 #[serde(skip_serializing_if = "String::is_empty")]
88 pub id: String,
89 pub name: String,
91 pub method: String,
93 pub path: String,
95 pub response: MockResponse,
97 #[serde(default = "default_true")]
99 pub enabled: bool,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub latency_ms: Option<u64>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub status_code: Option<u16>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub request_match: Option<RequestMatchCriteria>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub priority: Option<i32>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub scenario: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub required_scenario_state: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub new_scenario_state: Option<String>,
121}
122
123fn default_true() -> bool {
124 true
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MockResponse {
130 pub body: serde_json::Value,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub headers: Option<std::collections::HashMap<String, String>>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139pub struct RequestMatchCriteria {
140 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
142 pub headers: std::collections::HashMap<String, String>,
143 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
145 pub query_params: std::collections::HashMap<String, String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub body_pattern: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub json_path: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub xpath: Option<String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub custom_matcher: Option<String>,
158}
159
160pub fn mock_matches_request(
169 mock: &MockConfig,
170 method: &str,
171 path: &str,
172 headers: &std::collections::HashMap<String, String>,
173 query_params: &std::collections::HashMap<String, String>,
174 body: Option<&[u8]>,
175) -> bool {
176 use regex::Regex;
177
178 if !mock.enabled {
180 return false;
181 }
182
183 if mock.method.to_uppercase() != method.to_uppercase() {
185 return false;
186 }
187
188 if !path_matches_pattern(&mock.path, path) {
190 return false;
191 }
192
193 if let Some(criteria) = &mock.request_match {
195 for (key, expected_value) in &criteria.headers {
197 let header_key_lower = key.to_lowercase();
198 let found = headers.iter().find(|(k, _)| k.to_lowercase() == header_key_lower);
199
200 if let Some((_, actual_value)) = found {
201 if let Ok(re) = Regex::new(expected_value) {
203 if !re.is_match(actual_value) {
204 return false;
205 }
206 } else if actual_value != expected_value {
207 return false;
208 }
209 } else {
210 return false; }
212 }
213
214 for (key, expected_value) in &criteria.query_params {
216 if let Some(actual_value) = query_params.get(key) {
217 if actual_value != expected_value {
218 return false;
219 }
220 } else {
221 return false; }
223 }
224
225 if let Some(pattern) = &criteria.body_pattern {
227 if let Some(body_bytes) = body {
228 let body_str = String::from_utf8_lossy(body_bytes);
229 if let Ok(re) = Regex::new(pattern) {
231 if !re.is_match(&body_str) {
232 return false;
233 }
234 } else if body_str.as_ref() != pattern {
235 return false;
236 }
237 } else {
238 return false; }
240 }
241
242 if let Some(json_path) = &criteria.json_path {
244 if let Some(body_bytes) = body {
245 if let Ok(body_str) = std::str::from_utf8(body_bytes) {
246 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(body_str) {
247 if !json_path_exists(&json_value, json_path) {
249 return false;
250 }
251 }
252 }
253 }
254 }
255
256 if let Some(xpath) = &criteria.xpath {
258 if let Some(body_bytes) = body {
259 if let Ok(body_str) = std::str::from_utf8(body_bytes) {
260 if !xml_xpath_exists(body_str, xpath) {
261 return false;
262 }
263 } else {
264 return false;
265 }
266 } else {
267 return false; }
269 }
270
271 if let Some(custom) = &criteria.custom_matcher {
273 if !evaluate_custom_matcher(custom, method, path, headers, query_params, body) {
274 return false;
275 }
276 }
277 }
278
279 true
280}
281
282fn path_matches_pattern(pattern: &str, path: &str) -> bool {
284 if pattern == path {
286 return true;
287 }
288
289 if pattern == "*" {
291 return true;
292 }
293
294 let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
296 let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
297
298 if pattern_parts.len() != path_parts.len() {
299 if pattern.contains('*') {
301 return matches_wildcard_pattern(pattern, path);
302 }
303 return false;
304 }
305
306 for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
307 if pattern_part.starts_with('{') && pattern_part.ends_with('}') {
309 continue; }
311
312 if pattern_part != path_part {
313 return false;
314 }
315 }
316
317 true
318}
319
320fn matches_wildcard_pattern(pattern: &str, path: &str) -> bool {
322 use regex::Regex;
323
324 let regex_pattern = pattern.replace('*', ".*").replace('?', ".?");
326
327 if let Ok(re) = Regex::new(&format!("^{}$", regex_pattern)) {
328 return re.is_match(path);
329 }
330
331 false
332}
333
334fn json_path_exists(json: &serde_json::Value, json_path: &str) -> bool {
342 let path = if json_path == "$" {
343 return true;
344 } else if let Some(p) = json_path.strip_prefix("$.") {
345 p
346 } else if let Some(p) = json_path.strip_prefix('$') {
347 p.strip_prefix('.').unwrap_or(p)
348 } else {
349 json_path
350 };
351
352 let mut current = json;
353 for segment in split_json_path_segments(path) {
354 match segment {
355 JsonPathSegment::Field(name) => {
356 if let Some(obj) = current.as_object() {
357 if let Some(value) = obj.get(name) {
358 current = value;
359 } else {
360 return false;
361 }
362 } else {
363 return false;
364 }
365 }
366 JsonPathSegment::Index(idx) => {
367 if let Some(arr) = current.as_array() {
368 if let Some(value) = arr.get(idx) {
369 current = value;
370 } else {
371 return false;
372 }
373 } else {
374 return false;
375 }
376 }
377 JsonPathSegment::Wildcard => {
378 if let Some(arr) = current.as_array() {
379 return !arr.is_empty();
380 }
381 return false;
382 }
383 }
384 }
385 true
386}
387
388enum JsonPathSegment<'a> {
389 Field(&'a str),
390 Index(usize),
391 Wildcard,
392}
393
394fn split_json_path_segments(path: &str) -> Vec<JsonPathSegment<'_>> {
396 let mut segments = Vec::new();
397 for part in path.split('.') {
398 if part.is_empty() {
399 continue;
400 }
401 if let Some(bracket_start) = part.find('[') {
402 let field_name = &part[..bracket_start];
403 if !field_name.is_empty() {
404 segments.push(JsonPathSegment::Field(field_name));
405 }
406 let bracket_content = &part[bracket_start + 1..part.len() - 1];
407 if bracket_content == "*" {
408 segments.push(JsonPathSegment::Wildcard);
409 } else if let Ok(idx) = bracket_content.parse::<usize>() {
410 segments.push(JsonPathSegment::Index(idx));
411 }
412 } else {
413 segments.push(JsonPathSegment::Field(part));
414 }
415 }
416 segments
417}
418
419#[derive(Debug, Clone, PartialEq, Eq)]
420struct XPathSegment {
421 name: String,
422 text_equals: Option<String>,
423}
424
425fn parse_xpath_segment(segment: &str) -> Option<XPathSegment> {
426 if segment.is_empty() {
427 return None;
428 }
429
430 let trimmed = segment.trim();
431 if let Some(bracket_start) = trimmed.find('[') {
432 if !trimmed.ends_with(']') {
433 return None;
434 }
435
436 let name = trimmed[..bracket_start].trim();
437 let predicate = &trimmed[bracket_start + 1..trimmed.len() - 1];
438 let predicate = predicate.trim();
439
440 if let Some(raw) = predicate.strip_prefix("text()=") {
442 let raw = raw.trim();
443 if raw.len() >= 2
444 && ((raw.starts_with('"') && raw.ends_with('"'))
445 || (raw.starts_with('\'') && raw.ends_with('\'')))
446 {
447 let text = raw[1..raw.len() - 1].to_string();
448 if !name.is_empty() {
449 return Some(XPathSegment {
450 name: name.to_string(),
451 text_equals: Some(text),
452 });
453 }
454 }
455 }
456
457 None
458 } else {
459 Some(XPathSegment {
460 name: trimmed.to_string(),
461 text_equals: None,
462 })
463 }
464}
465
466fn segment_matches(node: roxmltree::Node<'_, '_>, segment: &XPathSegment) -> bool {
467 if !node.is_element() {
468 return false;
469 }
470 if node.tag_name().name() != segment.name {
471 return false;
472 }
473 match &segment.text_equals {
474 Some(expected) => node.text().map(str::trim).unwrap_or_default() == expected,
475 None => true,
476 }
477}
478
479fn xml_xpath_exists(xml_body: &str, xpath: &str) -> bool {
486 let doc = match roxmltree::Document::parse(xml_body) {
487 Ok(doc) => doc,
488 Err(err) => {
489 tracing::warn!("Failed to parse XML for XPath matching: {}", err);
490 return false;
491 }
492 };
493
494 let expr = xpath.trim();
495 if expr.is_empty() {
496 return false;
497 }
498
499 let (is_descendant, path_str) = if let Some(rest) = expr.strip_prefix("//") {
500 (true, rest)
501 } else if let Some(rest) = expr.strip_prefix('/') {
502 (false, rest)
503 } else {
504 tracing::warn!("Unsupported XPath expression (must start with / or //): {}", expr);
505 return false;
506 };
507
508 let segments: Vec<XPathSegment> = path_str
509 .split('/')
510 .filter(|s| !s.trim().is_empty())
511 .filter_map(parse_xpath_segment)
512 .collect();
513
514 if segments.is_empty() {
515 return false;
516 }
517
518 if is_descendant {
519 let first = &segments[0];
520 for node in doc.descendants().filter(|n| segment_matches(*n, first)) {
521 let mut frontier = vec![node];
522 for segment in &segments[1..] {
523 let mut next_frontier = Vec::new();
524 for parent in &frontier {
525 for child in parent.children().filter(|n| segment_matches(*n, segment)) {
526 next_frontier.push(child);
527 }
528 }
529 if next_frontier.is_empty() {
530 frontier.clear();
531 break;
532 }
533 frontier = next_frontier;
534 }
535 if !frontier.is_empty() {
536 return true;
537 }
538 }
539 false
540 } else {
541 let mut frontier = vec![doc.root_element()];
542 for (index, segment) in segments.iter().enumerate() {
543 let mut next_frontier = Vec::new();
544 for parent in &frontier {
545 if index == 0 {
546 if segment_matches(*parent, segment) {
547 next_frontier.push(*parent);
548 }
549 continue;
550 }
551 for child in parent.children().filter(|n| segment_matches(*n, segment)) {
552 next_frontier.push(child);
553 }
554 }
555 if next_frontier.is_empty() {
556 return false;
557 }
558 frontier = next_frontier;
559 }
560 !frontier.is_empty()
561 }
562}
563
564fn evaluate_custom_matcher(
566 expression: &str,
567 method: &str,
568 path: &str,
569 headers: &std::collections::HashMap<String, String>,
570 query_params: &std::collections::HashMap<String, String>,
571 body: Option<&[u8]>,
572) -> bool {
573 use regex::Regex;
574
575 let expr = expression.trim();
576
577 if expr.contains("==") {
579 let parts: Vec<&str> = expr.split("==").map(|s| s.trim()).collect();
580 if parts.len() != 2 {
581 return false;
582 }
583
584 let field = parts[0];
585 let expected_value = parts[1].trim_matches('"').trim_matches('\'');
586
587 match field {
588 "method" => method == expected_value,
589 "path" => path == expected_value,
590 _ if field.starts_with("headers.") => {
591 let header_name = &field[8..];
592 headers.get(header_name).map(|v| v == expected_value).unwrap_or(false)
593 }
594 _ if field.starts_with("query.") => {
595 let param_name = &field[6..];
596 query_params.get(param_name).map(|v| v == expected_value).unwrap_or(false)
597 }
598 _ => false,
599 }
600 }
601 else if expr.contains("=~") {
603 let parts: Vec<&str> = expr.split("=~").map(|s| s.trim()).collect();
604 if parts.len() != 2 {
605 return false;
606 }
607
608 let field = parts[0];
609 let pattern = parts[1].trim_matches('"').trim_matches('\'');
610
611 if let Ok(re) = Regex::new(pattern) {
612 match field {
613 "method" => re.is_match(method),
614 "path" => re.is_match(path),
615 _ if field.starts_with("headers.") => {
616 let header_name = &field[8..];
617 headers.get(header_name).map(|v| re.is_match(v)).unwrap_or(false)
618 }
619 _ if field.starts_with("query.") => {
620 let param_name = &field[6..];
621 query_params.get(param_name).map(|v| re.is_match(v)).unwrap_or(false)
622 }
623 _ => false,
624 }
625 } else {
626 false
627 }
628 }
629 else if expr.contains("contains") {
631 let parts: Vec<&str> = expr.split("contains").map(|s| s.trim()).collect();
632 if parts.len() != 2 {
633 return false;
634 }
635
636 let field = parts[0];
637 let search_value = parts[1].trim_matches('"').trim_matches('\'');
638
639 match field {
640 "path" => path.contains(search_value),
641 _ if field.starts_with("headers.") => {
642 let header_name = &field[8..];
643 headers.get(header_name).map(|v| v.contains(search_value)).unwrap_or(false)
644 }
645 _ if field.starts_with("body") => {
646 if let Some(body_bytes) = body {
647 let body_str = String::from_utf8_lossy(body_bytes);
648 body_str.contains(search_value)
649 } else {
650 false
651 }
652 }
653 _ => false,
654 }
655 } else {
656 tracing::warn!("Unknown custom matcher expression format: {}", expr);
658 false
659 }
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct ServerStats {
665 pub uptime_seconds: u64,
667 pub total_requests: u64,
669 pub active_mocks: usize,
671 pub enabled_mocks: usize,
673 pub registered_routes: usize,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct ServerConfig {
680 pub version: String,
682 pub port: u16,
684 pub has_openapi_spec: bool,
686 #[serde(skip_serializing_if = "Option::is_none")]
688 pub spec_path: Option<String>,
689}
690
691#[derive(Clone)]
693pub struct ManagementState {
694 pub mocks: Arc<RwLock<Vec<MockConfig>>>,
696 pub spec: Option<Arc<OpenApiSpec>>,
698 pub spec_path: Option<String>,
700 pub port: u16,
702 pub start_time: std::time::Instant,
704 pub request_counter: Arc<RwLock<u64>>,
706 pub proxy_config: Option<Arc<RwLock<ProxyConfig>>>,
708 #[cfg(feature = "smtp")]
710 pub smtp_registry: Option<Arc<mockforge_smtp::SmtpSpecRegistry>>,
711 #[cfg(feature = "mqtt")]
713 pub mqtt_broker: Option<Arc<mockforge_mqtt::MqttBroker>>,
714 #[cfg(feature = "kafka")]
716 pub kafka_broker: Option<Arc<mockforge_kafka::KafkaMockBroker>>,
717 #[cfg(any(feature = "mqtt", feature = "kafka"))]
719 pub message_events: Arc<broadcast::Sender<MessageEvent>>,
720 pub state_machine_manager:
722 Arc<RwLock<mockforge_scenarios::state_machine::ScenarioStateMachineManager>>,
723 pub ws_broadcast: Option<Arc<broadcast::Sender<crate::management_ws::MockEvent>>>,
725 pub lifecycle_hooks: Option<Arc<mockforge_core::lifecycle::LifecycleHookRegistry>>,
727 pub rule_explanations: Arc<
729 RwLock<
730 std::collections::HashMap<
731 String,
732 mockforge_core::intelligent_behavior::RuleExplanation,
733 >,
734 >,
735 >,
736 #[cfg(feature = "chaos")]
738 pub chaos_api_state: Option<Arc<mockforge_chaos::api::ChaosApiState>>,
739 pub server_config: Option<Arc<RwLock<mockforge_core::config::ServerConfig>>>,
741 #[cfg(feature = "conformance")]
743 pub conformance_state: crate::handlers::conformance::ConformanceState,
744}
745
746impl ManagementState {
747 pub fn new(spec: Option<Arc<OpenApiSpec>>, spec_path: Option<String>, port: u16) -> Self {
754 Self {
755 mocks: Arc::new(RwLock::new(Vec::new())),
756 spec,
757 spec_path,
758 port,
759 start_time: std::time::Instant::now(),
760 request_counter: Arc::new(RwLock::new(0)),
761 proxy_config: None,
762 #[cfg(feature = "smtp")]
763 smtp_registry: None,
764 #[cfg(feature = "mqtt")]
765 mqtt_broker: None,
766 #[cfg(feature = "kafka")]
767 kafka_broker: None,
768 #[cfg(any(feature = "mqtt", feature = "kafka"))]
769 message_events: {
770 let capacity = get_message_broadcast_capacity();
771 let (tx, _) = broadcast::channel(capacity);
772 Arc::new(tx)
773 },
774 state_machine_manager: Arc::new(RwLock::new(
775 mockforge_scenarios::state_machine::ScenarioStateMachineManager::new(),
776 )),
777 ws_broadcast: None,
778 lifecycle_hooks: None,
779 rule_explanations: Arc::new(RwLock::new(std::collections::HashMap::new())),
780 #[cfg(feature = "chaos")]
781 chaos_api_state: None,
782 server_config: None,
783 #[cfg(feature = "conformance")]
784 conformance_state: crate::handlers::conformance::ConformanceState::new(),
785 }
786 }
787
788 pub fn with_lifecycle_hooks(
790 mut self,
791 hooks: Arc<mockforge_core::lifecycle::LifecycleHookRegistry>,
792 ) -> Self {
793 self.lifecycle_hooks = Some(hooks);
794 self
795 }
796
797 pub fn with_ws_broadcast(
799 mut self,
800 ws_broadcast: Arc<broadcast::Sender<crate::management_ws::MockEvent>>,
801 ) -> Self {
802 self.ws_broadcast = Some(ws_broadcast);
803 self
804 }
805
806 pub fn with_proxy_config(mut self, proxy_config: Arc<RwLock<ProxyConfig>>) -> Self {
808 self.proxy_config = Some(proxy_config);
809 self
810 }
811
812 #[cfg(feature = "smtp")]
813 pub fn with_smtp_registry(
815 mut self,
816 smtp_registry: Arc<mockforge_smtp::SmtpSpecRegistry>,
817 ) -> Self {
818 self.smtp_registry = Some(smtp_registry);
819 self
820 }
821
822 #[cfg(feature = "mqtt")]
823 pub fn with_mqtt_broker(mut self, mqtt_broker: Arc<mockforge_mqtt::MqttBroker>) -> Self {
825 self.mqtt_broker = Some(mqtt_broker);
826 self
827 }
828
829 #[cfg(feature = "kafka")]
830 pub fn with_kafka_broker(
832 mut self,
833 kafka_broker: Arc<mockforge_kafka::KafkaMockBroker>,
834 ) -> Self {
835 self.kafka_broker = Some(kafka_broker);
836 self
837 }
838
839 #[cfg(feature = "chaos")]
840 pub fn with_chaos_api_state(
842 mut self,
843 chaos_api_state: Arc<mockforge_chaos::api::ChaosApiState>,
844 ) -> Self {
845 self.chaos_api_state = Some(chaos_api_state);
846 self
847 }
848
849 pub fn with_server_config(
851 mut self,
852 server_config: Arc<RwLock<mockforge_core::config::ServerConfig>>,
853 ) -> Self {
854 self.server_config = Some(server_config);
855 self
856 }
857}
858
859pub fn management_router(state: ManagementState) -> Router {
861 let router = Router::new()
862 .route("/capabilities", get(health::get_capabilities))
863 .route("/health", get(health::health_check))
864 .route("/stats", get(health::get_stats))
865 .route("/config", get(health::get_config))
866 .route("/config/validate", post(health::validate_config))
867 .route("/config/bulk", post(health::bulk_update_config))
868 .route("/mocks", get(mocks::list_mocks))
869 .route("/mocks", post(mocks::create_mock))
870 .route("/mocks/{id}", get(mocks::get_mock))
871 .route("/mocks/{id}", put(mocks::update_mock))
872 .route("/mocks/{id}", delete(mocks::delete_mock))
873 .route("/export", get(import_export::export_mocks))
874 .route("/import", post(import_export::import_mocks))
875 .route("/spec", get(health::get_openapi_spec));
876
877 #[cfg(feature = "smtp")]
878 let router = router
879 .route("/smtp/mailbox", get(protocols::list_smtp_emails))
880 .route("/smtp/mailbox", delete(protocols::clear_smtp_mailbox))
881 .route("/smtp/mailbox/{id}", get(protocols::get_smtp_email))
882 .route("/smtp/mailbox/export", get(protocols::export_smtp_mailbox))
883 .route("/smtp/mailbox/search", get(protocols::search_smtp_emails));
884
885 #[cfg(not(feature = "smtp"))]
886 let router = router;
887
888 #[cfg(feature = "mqtt")]
890 let router = router
891 .route("/mqtt/stats", get(protocols::get_mqtt_stats))
892 .route("/mqtt/clients", get(protocols::get_mqtt_clients))
893 .route("/mqtt/topics", get(protocols::get_mqtt_topics))
894 .route("/mqtt/clients/{client_id}", delete(protocols::disconnect_mqtt_client))
895 .route("/mqtt/messages/stream", get(protocols::mqtt_messages_stream))
896 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
897 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
898
899 #[cfg(not(feature = "mqtt"))]
900 let router = router
901 .route("/mqtt/publish", post(protocols::publish_mqtt_message_handler))
902 .route("/mqtt/publish/batch", post(protocols::publish_mqtt_batch_handler));
903
904 #[cfg(feature = "kafka")]
905 let router = router
906 .route("/kafka/stats", get(protocols::get_kafka_stats))
907 .route("/kafka/topics", get(protocols::get_kafka_topics))
908 .route("/kafka/topics/{topic}", get(protocols::get_kafka_topic))
909 .route("/kafka/groups", get(protocols::get_kafka_groups))
910 .route("/kafka/groups/{group_id}", get(protocols::get_kafka_group))
911 .route("/kafka/produce", post(protocols::produce_kafka_message))
912 .route("/kafka/produce/batch", post(protocols::produce_kafka_batch))
913 .route("/kafka/messages/stream", get(protocols::kafka_messages_stream));
914
915 #[cfg(not(feature = "kafka"))]
916 let router = router;
917
918 let router = router
920 .route("/migration/routes", get(migration::get_migration_routes))
921 .route("/migration/routes/{pattern}/toggle", post(migration::toggle_route_migration))
922 .route("/migration/routes/{pattern}", put(migration::set_route_migration_mode))
923 .route("/migration/groups/{group}/toggle", post(migration::toggle_group_migration))
924 .route("/migration/groups/{group}", put(migration::set_group_migration_mode))
925 .route("/migration/groups", get(migration::get_migration_groups))
926 .route("/migration/status", get(migration::get_migration_status));
927
928 let router = router
930 .route("/proxy/rules", get(proxy::list_proxy_rules))
931 .route("/proxy/rules", post(proxy::create_proxy_rule))
932 .route("/proxy/rules/{id}", get(proxy::get_proxy_rule))
933 .route("/proxy/rules/{id}", put(proxy::update_proxy_rule))
934 .route("/proxy/rules/{id}", delete(proxy::delete_proxy_rule))
935 .route("/proxy/inspect", get(proxy::get_proxy_inspect));
936
937 let router = router.route("/ai/generate-spec", post(ai_gen::generate_ai_spec));
939
940 let router = router.nest(
942 "/snapshot-diff",
943 crate::handlers::snapshot_diff::snapshot_diff_router(state.clone()),
944 );
945
946 #[cfg(feature = "behavioral-cloning")]
947 let router =
948 router.route("/mockai/generate-openapi", post(ai_gen::generate_openapi_from_traffic));
949
950 let router = router
951 .route("/mockai/learn", post(ai_gen::learn_from_examples))
952 .route("/mockai/rules/explanations", get(ai_gen::list_rule_explanations))
953 .route("/mockai/rules/{id}/explanation", get(ai_gen::get_rule_explanation))
954 .route("/chaos/config", get(ai_gen::get_chaos_config))
955 .route("/chaos/config", post(ai_gen::update_chaos_config))
956 .route("/network/profiles", get(ai_gen::list_network_profiles))
957 .route("/network/profile/apply", post(ai_gen::apply_network_profile));
958
959 let router =
961 router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
962
963 #[cfg(feature = "conformance")]
965 let router = router.nest_service(
966 "/conformance",
967 crate::handlers::conformance::conformance_router(state.conformance_state.clone()),
968 );
969 #[cfg(not(feature = "conformance"))]
970 let router = router;
971
972 router.with_state(state)
973}
974
975pub fn management_router_with_ui_builder(
977 state: ManagementState,
978 server_config: mockforge_core::config::ServerConfig,
979) -> Router {
980 use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
981
982 let management = management_router(state);
984
985 let ui_builder_state = UIBuilderState::new(server_config);
987 let ui_builder = create_ui_builder_router(ui_builder_state);
988
989 management.nest("/ui-builder", ui_builder)
991}
992
993pub fn management_router_with_spec_import(state: ManagementState) -> Router {
995 use crate::spec_import::{spec_import_router, SpecImportState};
996
997 let management = management_router(state);
999
1000 Router::new()
1002 .merge(management)
1003 .merge(spec_import_router(SpecImportState::new()))
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009
1010 #[tokio::test]
1011 async fn test_create_and_get_mock() {
1012 let state = ManagementState::new(None, None, 3000);
1013
1014 let mock = MockConfig {
1015 id: "test-1".to_string(),
1016 name: "Test Mock".to_string(),
1017 method: "GET".to_string(),
1018 path: "/test".to_string(),
1019 response: MockResponse {
1020 body: serde_json::json!({"message": "test"}),
1021 headers: None,
1022 },
1023 enabled: true,
1024 latency_ms: None,
1025 status_code: Some(200),
1026 request_match: None,
1027 priority: None,
1028 scenario: None,
1029 required_scenario_state: None,
1030 new_scenario_state: None,
1031 };
1032
1033 {
1035 let mut mocks = state.mocks.write().await;
1036 mocks.push(mock.clone());
1037 }
1038
1039 let mocks = state.mocks.read().await;
1041 let found = mocks.iter().find(|m| m.id == "test-1");
1042 assert!(found.is_some());
1043 assert_eq!(found.unwrap().name, "Test Mock");
1044 }
1045
1046 #[tokio::test]
1047 async fn test_server_stats() {
1048 let state = ManagementState::new(None, None, 3000);
1049
1050 {
1052 let mut mocks = state.mocks.write().await;
1053 mocks.push(MockConfig {
1054 id: "1".to_string(),
1055 name: "Mock 1".to_string(),
1056 method: "GET".to_string(),
1057 path: "/test1".to_string(),
1058 response: MockResponse {
1059 body: serde_json::json!({}),
1060 headers: None,
1061 },
1062 enabled: true,
1063 latency_ms: None,
1064 status_code: Some(200),
1065 request_match: None,
1066 priority: None,
1067 scenario: None,
1068 required_scenario_state: None,
1069 new_scenario_state: None,
1070 });
1071 mocks.push(MockConfig {
1072 id: "2".to_string(),
1073 name: "Mock 2".to_string(),
1074 method: "POST".to_string(),
1075 path: "/test2".to_string(),
1076 response: MockResponse {
1077 body: serde_json::json!({}),
1078 headers: None,
1079 },
1080 enabled: false,
1081 latency_ms: None,
1082 status_code: Some(201),
1083 request_match: None,
1084 priority: None,
1085 scenario: None,
1086 required_scenario_state: None,
1087 new_scenario_state: None,
1088 });
1089 }
1090
1091 let mocks = state.mocks.read().await;
1092 assert_eq!(mocks.len(), 2);
1093 assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
1094 }
1095
1096 #[test]
1097 fn test_mock_matches_request_with_xpath_absolute_path() {
1098 let mock = MockConfig {
1099 id: "xpath-1".to_string(),
1100 name: "XPath Match".to_string(),
1101 method: "POST".to_string(),
1102 path: "/xml".to_string(),
1103 response: MockResponse {
1104 body: serde_json::json!({"ok": true}),
1105 headers: None,
1106 },
1107 enabled: true,
1108 latency_ms: None,
1109 status_code: Some(200),
1110 request_match: Some(RequestMatchCriteria {
1111 xpath: Some("/root/order/id".to_string()),
1112 ..Default::default()
1113 }),
1114 priority: None,
1115 scenario: None,
1116 required_scenario_state: None,
1117 new_scenario_state: None,
1118 };
1119
1120 let body = br#"<root><order><id>123</id></order></root>"#;
1121 let headers = std::collections::HashMap::new();
1122 let query = std::collections::HashMap::new();
1123
1124 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1125 }
1126
1127 #[test]
1128 fn test_mock_matches_request_with_xpath_text_predicate() {
1129 let mock = MockConfig {
1130 id: "xpath-2".to_string(),
1131 name: "XPath Predicate Match".to_string(),
1132 method: "POST".to_string(),
1133 path: "/xml".to_string(),
1134 response: MockResponse {
1135 body: serde_json::json!({"ok": true}),
1136 headers: None,
1137 },
1138 enabled: true,
1139 latency_ms: None,
1140 status_code: Some(200),
1141 request_match: Some(RequestMatchCriteria {
1142 xpath: Some("//order/id[text()='123']".to_string()),
1143 ..Default::default()
1144 }),
1145 priority: None,
1146 scenario: None,
1147 required_scenario_state: None,
1148 new_scenario_state: None,
1149 };
1150
1151 let body = br#"<root><order><id>123</id></order></root>"#;
1152 let headers = std::collections::HashMap::new();
1153 let query = std::collections::HashMap::new();
1154
1155 assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1156 }
1157
1158 #[test]
1159 fn test_mock_matches_request_with_xpath_no_match() {
1160 let mock = MockConfig {
1161 id: "xpath-3".to_string(),
1162 name: "XPath No Match".to_string(),
1163 method: "POST".to_string(),
1164 path: "/xml".to_string(),
1165 response: MockResponse {
1166 body: serde_json::json!({"ok": true}),
1167 headers: None,
1168 },
1169 enabled: true,
1170 latency_ms: None,
1171 status_code: Some(200),
1172 request_match: Some(RequestMatchCriteria {
1173 xpath: Some("//order/id[text()='456']".to_string()),
1174 ..Default::default()
1175 }),
1176 priority: None,
1177 scenario: None,
1178 required_scenario_state: None,
1179 new_scenario_state: None,
1180 };
1181
1182 let body = br#"<root><order><id>123</id></order></root>"#;
1183 let headers = std::collections::HashMap::new();
1184 let query = std::collections::HashMap::new();
1185
1186 assert!(!mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1187 }
1188}