Skip to main content

mockforge_http/management/
mod.rs

1/// Management API for MockForge
2///
3/// Provides REST endpoints for controlling mocks, server configuration,
4/// and integration with developer tools (VS Code extension, CI/CD, etc.)
5mod 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/// Default broadcast channel capacity for message events
29#[cfg(any(feature = "mqtt", feature = "kafka"))]
30const DEFAULT_MESSAGE_BROADCAST_CAPACITY: usize = 1000;
31
32/// Get the broadcast channel capacity from environment or use default
33#[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/// Message event types for real-time monitoring
42#[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 message event
48    Mqtt(MqttMessageEvent),
49    #[cfg(feature = "kafka")]
50    /// Kafka message event
51    Kafka(KafkaMessageEvent),
52}
53
54#[cfg(feature = "mqtt")]
55/// MQTT message event for real-time monitoring
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct MqttMessageEvent {
58    /// MQTT topic name
59    pub topic: String,
60    /// Message payload content
61    pub payload: String,
62    /// Quality of Service level (0, 1, or 2)
63    pub qos: u8,
64    /// Whether the message is retained
65    pub retain: bool,
66    /// RFC3339 formatted timestamp
67    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/// Mock configuration representation
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct MockConfig {
86    /// Unique identifier for the mock
87    #[serde(skip_serializing_if = "String::is_empty")]
88    pub id: String,
89    /// Human-readable name for the mock
90    pub name: String,
91    /// HTTP method (GET, POST, etc.)
92    pub method: String,
93    /// API path pattern to match
94    pub path: String,
95    /// Response configuration
96    pub response: MockResponse,
97    /// Whether this mock is currently enabled
98    #[serde(default = "default_true")]
99    pub enabled: bool,
100    /// Optional latency to inject in milliseconds
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub latency_ms: Option<u64>,
103    /// Optional HTTP status code override
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub status_code: Option<u16>,
106    /// Request matching criteria (headers, query params, body patterns)
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub request_match: Option<RequestMatchCriteria>,
109    /// Priority for mock ordering (higher priority mocks are matched first)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub priority: Option<i32>,
112    /// Scenario name for stateful mocking
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub scenario: Option<String>,
115    /// Required scenario state for this mock to be active
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub required_scenario_state: Option<String>,
118    /// New scenario state after this mock is matched
119    #[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/// Mock response configuration
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MockResponse {
130    /// Response body as JSON
131    pub body: serde_json::Value,
132    /// Optional custom response headers
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub headers: Option<std::collections::HashMap<String, String>>,
135}
136
137/// Request matching criteria for advanced request matching
138#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139pub struct RequestMatchCriteria {
140    /// Headers that must be present and match (case-insensitive header names)
141    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
142    pub headers: std::collections::HashMap<String, String>,
143    /// Query parameters that must be present and match
144    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
145    pub query_params: std::collections::HashMap<String, String>,
146    /// Request body pattern (supports exact match or regex)
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub body_pattern: Option<String>,
149    /// JSONPath expression for JSON body matching
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub json_path: Option<String>,
152    /// XPath expression for XML body matching
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub xpath: Option<String>,
155    /// Custom matcher expression (e.g., "headers.content-type == \"application/json\"")
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub custom_matcher: Option<String>,
158}
159
160/// Check if a request matches the given mock configuration
161///
162/// This function implements comprehensive request matching including:
163/// - Method and path matching
164/// - Header matching (with regex support)
165/// - Query parameter matching
166/// - Body pattern matching (exact, regex, JSONPath, XPath)
167/// - Custom matcher expressions
168pub 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    // Check if mock is enabled
179    if !mock.enabled {
180        return false;
181    }
182
183    // Check method (case-insensitive)
184    if mock.method.to_uppercase() != method.to_uppercase() {
185        return false;
186    }
187
188    // Check path pattern (supports wildcards and path parameters)
189    if !path_matches_pattern(&mock.path, path) {
190        return false;
191    }
192
193    // Check request matching criteria if present
194    if let Some(criteria) = &mock.request_match {
195        // Check headers
196        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                // Try regex match first, then exact match
202                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; // Header not found
211            }
212        }
213
214        // Check query parameters
215        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; // Query param not found
222            }
223        }
224
225        // Check body pattern
226        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                // Try regex first, then exact match
230                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; // Body required but not present
239            }
240        }
241
242        // Check JSONPath (simplified implementation)
243        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                        // Simple JSONPath check
248                        if !json_path_exists(&json_value, json_path) {
249                            return false;
250                        }
251                    }
252                }
253            }
254        }
255
256        // Check XPath (supports a focused subset)
257        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; // Body required but not present
268            }
269        }
270
271        // Check custom matcher
272        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
282/// Check if a path matches a pattern (supports wildcards and path parameters)
283fn path_matches_pattern(pattern: &str, path: &str) -> bool {
284    // Exact match
285    if pattern == path {
286        return true;
287    }
288
289    // Wildcard match
290    if pattern == "*" {
291        return true;
292    }
293
294    // Path parameter matching (e.g., /users/{id} matches /users/123)
295    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        // Check for wildcard patterns
300        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        // Check for path parameters {param}
308        if pattern_part.starts_with('{') && pattern_part.ends_with('}') {
309            continue; // Matches any value
310        }
311
312        if pattern_part != path_part {
313            return false;
314        }
315    }
316
317    true
318}
319
320/// Check if path matches a wildcard pattern
321fn matches_wildcard_pattern(pattern: &str, path: &str) -> bool {
322    use regex::Regex;
323
324    // Convert pattern to regex
325    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
334/// Check if a JSONPath exists in a JSON value
335///
336/// Supports:
337/// - `$` — root element
338/// - `$.field.subfield` — nested object access
339/// - `$.items[0].name` — array index access
340/// - `$.items[*]` — array wildcard (checks array is non-empty)
341fn 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
394/// Split a JSONPath (without the leading `$`) into segments
395fn 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        // Support simple predicate: [text()="value"] or [text()='value']
441        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
479/// Check if an XPath expression matches an XML body.
480///
481/// Supported subset:
482/// - Absolute paths: `/root/child/item`
483/// - Descendant search: `//item` and `//parent/child`
484/// - Optional text predicate per segment: `item[text()="value"]`
485fn 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
564/// Evaluate a custom matcher expression
565fn 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    // Handle equality expressions (field == "value")
578    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    // Handle regex match expressions (field =~ "pattern")
602    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    // Handle contains expressions (field contains "value")
630    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        // Unknown expression format
657        tracing::warn!("Unknown custom matcher expression format: {}", expr);
658        false
659    }
660}
661
662/// Server statistics
663#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct ServerStats {
665    /// Server uptime in seconds
666    pub uptime_seconds: u64,
667    /// Total number of requests processed
668    pub total_requests: u64,
669    /// Number of active mock configurations
670    pub active_mocks: usize,
671    /// Number of currently enabled mocks
672    pub enabled_mocks: usize,
673    /// Number of registered API routes
674    pub registered_routes: usize,
675}
676
677/// Server configuration info
678#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct ServerConfig {
680    /// MockForge version string
681    pub version: String,
682    /// Server port number
683    pub port: u16,
684    /// Whether an OpenAPI spec is loaded
685    pub has_openapi_spec: bool,
686    /// Optional path to the OpenAPI spec file
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub spec_path: Option<String>,
689}
690
691/// Shared state for the management API
692#[derive(Clone)]
693pub struct ManagementState {
694    /// Collection of mock configurations
695    pub mocks: Arc<RwLock<Vec<MockConfig>>>,
696    /// Optional OpenAPI specification
697    pub spec: Option<Arc<OpenApiSpec>>,
698    /// Optional path to the OpenAPI spec file
699    pub spec_path: Option<String>,
700    /// Server port number
701    pub port: u16,
702    /// Server start time for uptime calculation
703    pub start_time: std::time::Instant,
704    /// Counter for total requests processed
705    pub request_counter: Arc<RwLock<u64>>,
706    /// Optional proxy configuration for migration pipeline
707    pub proxy_config: Option<Arc<RwLock<ProxyConfig>>>,
708    /// Optional SMTP registry for email mocking
709    #[cfg(feature = "smtp")]
710    pub smtp_registry: Option<Arc<mockforge_smtp::SmtpSpecRegistry>>,
711    /// Optional MQTT broker for message mocking
712    #[cfg(feature = "mqtt")]
713    pub mqtt_broker: Option<Arc<mockforge_mqtt::MqttBroker>>,
714    /// Optional Kafka broker for event streaming
715    #[cfg(feature = "kafka")]
716    pub kafka_broker: Option<Arc<mockforge_kafka::KafkaMockBroker>>,
717    /// Broadcast channel for message events (MQTT & Kafka)
718    #[cfg(any(feature = "mqtt", feature = "kafka"))]
719    pub message_events: Arc<broadcast::Sender<MessageEvent>>,
720    /// State machine manager for scenario state machines
721    pub state_machine_manager:
722        Arc<RwLock<mockforge_scenarios::state_machine::ScenarioStateMachineManager>>,
723    /// Optional WebSocket broadcast channel for real-time updates
724    pub ws_broadcast: Option<Arc<broadcast::Sender<crate::management_ws::MockEvent>>>,
725    /// Lifecycle hook registry for extensibility
726    pub lifecycle_hooks: Option<Arc<mockforge_core::lifecycle::LifecycleHookRegistry>>,
727    /// Rule explanations storage (in-memory for now)
728    pub rule_explanations: Arc<
729        RwLock<
730            std::collections::HashMap<
731                String,
732                mockforge_core::intelligent_behavior::RuleExplanation,
733            >,
734        >,
735    >,
736    /// Optional chaos API state for chaos config management
737    #[cfg(feature = "chaos")]
738    pub chaos_api_state: Option<Arc<mockforge_chaos::api::ChaosApiState>>,
739    /// Optional server configuration for profile application
740    pub server_config: Option<Arc<RwLock<mockforge_core::config::ServerConfig>>>,
741    /// Conformance testing state
742    #[cfg(feature = "conformance")]
743    pub conformance_state: crate::handlers::conformance::ConformanceState,
744}
745
746impl ManagementState {
747    /// Create a new management state
748    ///
749    /// # Arguments
750    /// * `spec` - Optional OpenAPI specification
751    /// * `spec_path` - Optional path to the OpenAPI spec file
752    /// * `port` - Server port number
753    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    /// Add lifecycle hook registry to management state
789    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    /// Add WebSocket broadcast channel to management state
798    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    /// Add proxy configuration to management state
807    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    /// Add SMTP registry to management state
814    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    /// Add MQTT broker to management state
824    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    /// Add Kafka broker to management state
831    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    /// Add chaos API state to management state
841    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    /// Add server configuration to management state
850    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
859/// Build the management API router
860pub 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    // MQTT routes
889    #[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    // Migration pipeline routes
919    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    // Proxy replacement rules routes
929    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    // AI-powered features
938    let router = router.route("/ai/generate-spec", post(ai_gen::generate_ai_spec));
939
940    // Snapshot diff endpoints
941    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    // State machine API routes
960    let router =
961        router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
962
963    // Conformance testing API routes
964    #[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
975/// Build the management API router with UI Builder support
976pub 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    // Create the base management router
983    let management = management_router(state);
984
985    // Create UI Builder state and router
986    let ui_builder_state = UIBuilderState::new(server_config);
987    let ui_builder = create_ui_builder_router(ui_builder_state);
988
989    // Nest UI Builder under /ui-builder
990    management.nest("/ui-builder", ui_builder)
991}
992
993/// Build management router with spec import API
994pub fn management_router_with_spec_import(state: ManagementState) -> Router {
995    use crate::spec_import::{spec_import_router, SpecImportState};
996
997    // Create base management router
998    let management = management_router(state);
999
1000    // Merge with spec import router
1001    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        // Create mock
1034        {
1035            let mut mocks = state.mocks.write().await;
1036            mocks.push(mock.clone());
1037        }
1038
1039        // Get mock
1040        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        // Add some mocks
1051        {
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}