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    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/// Default broadcast channel capacity for message events
33#[cfg(any(feature = "mqtt", feature = "kafka"))]
34const DEFAULT_MESSAGE_BROADCAST_CAPACITY: usize = 1000;
35
36/// Get the broadcast channel capacity from environment or use default
37#[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/// Message event types for real-time monitoring
46#[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 message event
52    Mqtt(MqttMessageEvent),
53    #[cfg(feature = "kafka")]
54    /// Kafka message event
55    Kafka(KafkaMessageEvent),
56}
57
58#[cfg(feature = "mqtt")]
59/// MQTT message event for real-time monitoring
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct MqttMessageEvent {
62    /// MQTT topic name
63    pub topic: String,
64    /// Message payload content
65    pub payload: String,
66    /// Quality of Service level (0, 1, or 2)
67    pub qos: u8,
68    /// Whether the message is retained
69    pub retain: bool,
70    /// RFC3339 formatted timestamp
71    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/// Mock configuration representation
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct MockConfig {
90    /// Unique identifier for the mock. Auto-generated by `create_mock` when
91    /// omitted on input — clients can POST without an `id` and the server
92    /// will assign one.
93    #[serde(default, skip_serializing_if = "String::is_empty")]
94    pub id: String,
95    /// Human-readable name for the mock. Optional on input; defaults to an
96    /// empty string (consumers can leave it blank when programmatically
97    /// creating mocks via the management API).
98    #[serde(default)]
99    pub name: String,
100    /// HTTP method (GET, POST, etc.)
101    pub method: String,
102    /// API path pattern to match
103    pub path: String,
104    /// Response configuration
105    pub response: MockResponse,
106    /// Whether this mock is currently enabled
107    #[serde(default = "default_true")]
108    pub enabled: bool,
109    /// Optional latency to inject in milliseconds
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub latency_ms: Option<u64>,
112    /// Optional HTTP status code override
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub status_code: Option<u16>,
115    /// Request matching criteria (headers, query params, body patterns)
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub request_match: Option<RequestMatchCriteria>,
118    /// Priority for mock ordering (higher priority mocks are matched first)
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub priority: Option<i32>,
121    /// Scenario name for stateful mocking
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub scenario: Option<String>,
124    /// Required scenario state for this mock to be active
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub required_scenario_state: Option<String>,
127    /// New scenario state after this mock is matched
128    #[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/// Mock response configuration
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct MockResponse {
139    /// Response body as JSON
140    pub body: serde_json::Value,
141    /// Optional custom response headers
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub headers: Option<std::collections::HashMap<String, String>>,
144}
145
146/// Request matching criteria for advanced request matching
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148pub struct RequestMatchCriteria {
149    /// Headers that must be present and match (case-insensitive header names)
150    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
151    pub headers: std::collections::HashMap<String, String>,
152    /// Query parameters that must be present and match
153    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
154    pub query_params: std::collections::HashMap<String, String>,
155    /// Request body pattern (supports exact match or regex)
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub body_pattern: Option<String>,
158    /// JSONPath expression for JSON body matching
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub json_path: Option<String>,
161    /// XPath expression for XML body matching
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub xpath: Option<String>,
164    /// Custom matcher expression (e.g., "headers.content-type == \"application/json\"")
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub custom_matcher: Option<String>,
167}
168
169/// Check if a request matches the given mock configuration
170///
171/// This function implements comprehensive request matching including:
172/// - Method and path matching
173/// - Header matching (with regex support)
174/// - Query parameter matching
175/// - Body pattern matching (exact, regex, JSONPath, XPath)
176/// - Custom matcher expressions
177pub 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    // Check if mock is enabled
188    if !mock.enabled {
189        return false;
190    }
191
192    // Check method (case-insensitive)
193    if mock.method.to_uppercase() != method.to_uppercase() {
194        return false;
195    }
196
197    // Check path pattern (supports wildcards and path parameters)
198    if !path_matches_pattern(&mock.path, path) {
199        return false;
200    }
201
202    // Check request matching criteria if present
203    if let Some(criteria) = &mock.request_match {
204        // Check headers
205        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                // Try regex match first, then exact match
211                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; // Header not found
220            }
221        }
222
223        // Check query parameters
224        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; // Query param not found
231            }
232        }
233
234        // Check body pattern
235        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                // Try regex first, then exact match
239                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; // Body required but not present
248            }
249        }
250
251        // Check JSONPath (simplified implementation)
252        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                        // Simple JSONPath check
257                        if !json_path_exists(&json_value, json_path) {
258                            return false;
259                        }
260                    }
261                }
262            }
263        }
264
265        // Check XPath (supports a focused subset)
266        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; // Body required but not present
277            }
278        }
279
280        // Check custom matcher
281        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
291/// Check if a path matches a pattern (supports wildcards and path parameters)
292fn path_matches_pattern(pattern: &str, path: &str) -> bool {
293    // Exact match
294    if pattern == path {
295        return true;
296    }
297
298    // Wildcard match
299    if pattern == "*" {
300        return true;
301    }
302
303    // Path parameter matching (e.g., /users/{id} matches /users/123)
304    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        // Check for wildcard patterns
309        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        // Check for path parameters {param}
317        if pattern_part.starts_with('{') && pattern_part.ends_with('}') {
318            continue; // Matches any value
319        }
320
321        if pattern_part != path_part {
322            return false;
323        }
324    }
325
326    true
327}
328
329/// Check if path matches a wildcard pattern
330fn matches_wildcard_pattern(pattern: &str, path: &str) -> bool {
331    use regex::Regex;
332
333    // Convert pattern to regex
334    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
343/// Check if a JSONPath exists in a JSON value
344///
345/// Supports:
346/// - `$` — root element
347/// - `$.field.subfield` — nested object access
348/// - `$.items[0].name` — array index access
349/// - `$.items[*]` — array wildcard (checks array is non-empty)
350fn 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
403/// Split a JSONPath (without the leading `$`) into segments
404fn 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        // Support simple predicate: [text()="value"] or [text()='value']
450        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
488/// Check if an XPath expression matches an XML body.
489///
490/// Supported subset:
491/// - Absolute paths: `/root/child/item`
492/// - Descendant search: `//item` and `//parent/child`
493/// - Optional text predicate per segment: `item[text()="value"]`
494fn 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
573/// Evaluate a custom matcher expression
574fn 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    // Handle equality expressions (field == "value")
587    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    // Handle regex match expressions (field =~ "pattern")
611    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    // Handle contains expressions (field contains "value")
639    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        // Unknown expression format
666        tracing::warn!("Unknown custom matcher expression format: {}", expr);
667        false
668    }
669}
670
671/// Server statistics
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct ServerStats {
674    /// Server uptime in seconds
675    pub uptime_seconds: u64,
676    /// Total number of requests processed
677    pub total_requests: u64,
678    /// Number of active mock configurations
679    pub active_mocks: usize,
680    /// Number of currently enabled mocks
681    pub enabled_mocks: usize,
682    /// Number of registered API routes
683    pub registered_routes: usize,
684}
685
686/// Server configuration info
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct ServerConfig {
689    /// MockForge version string
690    pub version: String,
691    /// Server port number
692    pub port: u16,
693    /// Whether an OpenAPI spec is loaded
694    pub has_openapi_spec: bool,
695    /// Optional path to the OpenAPI spec file
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub spec_path: Option<String>,
698}
699
700/// Shared state for the management API
701#[derive(Clone)]
702pub struct ManagementState {
703    /// Collection of mock configurations
704    pub mocks: Arc<RwLock<Vec<MockConfig>>>,
705    /// Optional OpenAPI specification
706    pub spec: Option<Arc<OpenApiSpec>>,
707    /// Optional path to the OpenAPI spec file
708    pub spec_path: Option<String>,
709    /// Server port number
710    pub port: u16,
711    /// Server start time for uptime calculation
712    pub start_time: std::time::Instant,
713    /// Counter for total requests processed
714    pub request_counter: Arc<RwLock<u64>>,
715    /// Optional proxy configuration for migration pipeline
716    pub proxy_config: Option<Arc<RwLock<ProxyConfig>>>,
717    /// Optional SMTP registry for email mocking
718    #[cfg(feature = "smtp")]
719    pub smtp_registry: Option<Arc<mockforge_smtp::SmtpSpecRegistry>>,
720    /// Optional MQTT broker for message mocking
721    #[cfg(feature = "mqtt")]
722    pub mqtt_broker: Option<Arc<mockforge_mqtt::MqttBroker>>,
723    /// Optional Kafka broker for event streaming
724    #[cfg(feature = "kafka")]
725    pub kafka_broker: Option<Arc<mockforge_kafka::KafkaMockBroker>>,
726    /// Broadcast channel for message events (MQTT & Kafka)
727    #[cfg(any(feature = "mqtt", feature = "kafka"))]
728    pub message_events: Arc<broadcast::Sender<MessageEvent>>,
729    /// State machine manager for scenario state machines
730    pub state_machine_manager:
731        Arc<RwLock<mockforge_scenarios::state_machine::ScenarioStateMachineManager>>,
732    /// Optional WebSocket broadcast channel for real-time updates
733    pub ws_broadcast: Option<Arc<broadcast::Sender<crate::management_ws::MockEvent>>>,
734    /// Lifecycle hook registry for extensibility
735    pub lifecycle_hooks: Option<Arc<mockforge_core::lifecycle::LifecycleHookRegistry>>,
736    /// Rule explanations storage (in-memory for now)
737    pub rule_explanations: Arc<
738        RwLock<
739            std::collections::HashMap<
740                String,
741                mockforge_foundation::intelligent_behavior::rule_types::RuleExplanation,
742            >,
743        >,
744    >,
745    /// Optional chaos API state for chaos config management
746    #[cfg(feature = "chaos")]
747    pub chaos_api_state: Option<Arc<mockforge_chaos::api::ChaosApiState>>,
748    /// Optional server configuration for profile application
749    pub server_config: Option<Arc<RwLock<mockforge_core::config::ServerConfig>>>,
750    /// Conformance testing state
751    #[cfg(feature = "conformance")]
752    pub conformance_state: crate::handlers::conformance::ConformanceState,
753}
754
755impl ManagementState {
756    /// Create a new management state
757    ///
758    /// # Arguments
759    /// * `spec` - Optional OpenAPI specification
760    /// * `spec_path` - Optional path to the OpenAPI spec file
761    /// * `port` - Server port number
762    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    /// Add lifecycle hook registry to management state
798    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    /// Add WebSocket broadcast channel to management state
807    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    /// Add proxy configuration to management state
816    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    /// Add SMTP registry to management state
823    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    /// Add MQTT broker to management state
833    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    /// Add Kafka broker to management state
840    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    /// Add chaos API state to management state
850    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    /// Add server configuration to management state
859    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
868/// Build the management API router
869pub fn management_router(state: ManagementState) -> Router {
870    let router = Router::new()
871        .route("/capabilities", get(get_capabilities))
872        .route("/health", get(health_check))
873        .route("/stats", get(get_stats))
874        .route("/config", get(get_config))
875        .route("/config/validate", post(validate_config))
876        .route("/config/bulk", post(bulk_update_config))
877        .route("/mocks", get(mocks::list_mocks))
878        .route("/mocks", post(mocks::create_mock))
879        .route("/mocks/{id}", get(mocks::get_mock))
880        .route("/mocks/{id}", put(mocks::update_mock))
881        .route("/mocks/{id}", delete(mocks::delete_mock))
882        .route("/export", get(export_mocks))
883        .route("/import", post(import_mocks))
884        .route("/spec", get(get_openapi_spec));
885
886    #[cfg(feature = "smtp")]
887    let router = router
888        .route("/smtp/mailbox", get(protocols::list_smtp_emails))
889        .route("/smtp/mailbox", delete(protocols::clear_smtp_mailbox))
890        .route("/smtp/mailbox/{id}", get(protocols::get_smtp_email))
891        .route("/smtp/mailbox/export", get(protocols::export_smtp_mailbox))
892        .route("/smtp/mailbox/search", get(protocols::search_smtp_emails));
893
894    #[cfg(not(feature = "smtp"))]
895    let router = router;
896
897    // MQTT routes
898    #[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    // Migration pipeline routes
928    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    // Proxy replacement rules routes
938    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    // AI-powered features
947    let router = router.route("/ai/generate-spec", post(generate_ai_spec));
948
949    // Snapshot diff endpoints
950    let router = router.nest(
951        "/snapshot-diff",
952        crate::handlers::snapshot_diff::snapshot_diff_router(state.clone()),
953    );
954
955    #[cfg(feature = "behavioral-cloning")]
956    let router = router.route("/mockai/generate-openapi", post(generate_openapi_from_traffic));
957
958    let router = router
959        .route("/mockai/learn", post(learn_from_examples))
960        .route("/mockai/rules/explanations", get(list_rule_explanations))
961        .route("/mockai/rules/{id}/explanation", get(get_rule_explanation))
962        .route("/chaos/config", get(get_chaos_config))
963        .route("/chaos/config", post(update_chaos_config))
964        .route("/network/profiles", get(list_network_profiles))
965        .route("/network/profile/apply", post(apply_network_profile));
966
967    // State machine API routes
968    let router =
969        router.nest("/state-machines", crate::state_machine_api::create_state_machine_routes());
970
971    // Conformance testing API routes
972    #[cfg(feature = "conformance")]
973    let router = router.nest_service(
974        "/conformance",
975        crate::handlers::conformance::conformance_router(state.conformance_state.clone()),
976    );
977    #[cfg(not(feature = "conformance"))]
978    let router = router;
979
980    router.with_state(state)
981}
982
983/// Build the management API router with UI Builder support
984pub fn management_router_with_ui_builder(
985    state: ManagementState,
986    server_config: mockforge_core::config::ServerConfig,
987) -> Router {
988    use crate::ui_builder::{create_ui_builder_router, UIBuilderState};
989
990    // Create the base management router
991    let management = management_router(state);
992
993    // Create UI Builder state and router
994    let ui_builder_state = UIBuilderState::new(server_config);
995    let ui_builder = create_ui_builder_router(ui_builder_state);
996
997    // Nest UI Builder under /ui-builder
998    management.nest("/ui-builder", ui_builder)
999}
1000
1001/// Build management router with spec import API
1002pub fn management_router_with_spec_import(state: ManagementState) -> Router {
1003    use crate::spec_import::{spec_import_router, SpecImportState};
1004
1005    // Create base management router
1006    let management = management_router(state);
1007
1008    // Merge with spec import router
1009    Router::new()
1010        .merge(management)
1011        .merge(spec_import_router(SpecImportState::new()))
1012}
1013
1014/// Match an incoming request against mocks registered via the
1015/// `POST /__mockforge/api/mocks` endpoint and return the first match's
1016/// response (ordered by descending priority). Returns `None` if nothing
1017/// matches, so the caller can chain it with its existing 404.
1018///
1019/// This is what lets the `@mockforge-dev/sdk` Node.js SDK register a stub
1020/// dynamically and have subsequent HTTP requests actually hit it — without
1021/// it, stubs live in `ManagementState` but no route dispatches to them, so
1022/// every SDK test would 404.
1023pub async fn serve_dynamic_mock(state: &ManagementState, req: Request<Body>) -> Option<Response> {
1024    let method = req.method().as_str().to_string();
1025    let path = req.uri().path().to_string();
1026
1027    let query_params: std::collections::HashMap<String, String> = req
1028        .uri()
1029        .query()
1030        .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
1031        .unwrap_or_default();
1032
1033    let headers: std::collections::HashMap<String, String> = req
1034        .headers()
1035        .iter()
1036        .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str().to_string(), v.to_string())))
1037        .collect();
1038
1039    let (_parts, body) = req.into_parts();
1040    let body_bytes = axum::body::to_bytes(body, 1024 * 1024).await.ok()?;
1041    let body_opt: Option<&[u8]> = if body_bytes.is_empty() {
1042        None
1043    } else {
1044        Some(&body_bytes)
1045    };
1046
1047    let mocks = state.mocks.read().await;
1048
1049    let mut candidates: Vec<&MockConfig> = mocks
1050        .iter()
1051        .filter(|m| mock_matches_request(m, &method, &path, &headers, &query_params, body_opt))
1052        .collect();
1053    if candidates.is_empty() {
1054        return None;
1055    }
1056    candidates.sort_by_key(|m| -(m.priority.unwrap_or(0)));
1057    let mock = candidates.first()?;
1058
1059    if let Some(ms) = mock.latency_ms {
1060        if ms > 0 {
1061            tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1062        }
1063    }
1064
1065    let status = mock
1066        .status_code
1067        .and_then(|c| StatusCode::from_u16(c).ok())
1068        .unwrap_or(StatusCode::OK);
1069
1070    // Honor MOCKFORGE_RESPONSE_TEMPLATE_EXPAND for mocks created via the
1071    // management API. The full templater in mockforge-core handles
1072    // `{{faker.email}}`, `{{uuid}}`, `{{randInt …}}`, etc. — it's gated
1073    // on the env var because it's the same opt-in as everywhere else
1074    // (config-loaded routes, OpenAPI overrides). Run inside spawn_blocking
1075    // because the templater's rng() and faker providers are not Send-safe
1076    // across `.await` points.
1077    let template_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1078        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1079        .unwrap_or(false);
1080    let body_value = if template_expand {
1081        let body_clone = mock.response.body.clone();
1082        match tokio::task::spawn_blocking(move || {
1083            mockforge_core::templating::expand_tokens(&body_clone)
1084        })
1085        .await
1086        {
1087            Ok(expanded) => expanded,
1088            Err(_) => mock.response.body.clone(),
1089        }
1090    } else {
1091        mock.response.body.clone()
1092    };
1093
1094    let body_bytes_out = serde_json::to_vec(&body_value).unwrap_or_default();
1095    let mut response = Response::builder().status(status);
1096
1097    let mut has_content_type = false;
1098    if let Some(h) = &mock.response.headers {
1099        for (k, v) in h {
1100            if k.eq_ignore_ascii_case("content-type") {
1101                has_content_type = true;
1102            }
1103            if let (Ok(name), Ok(value)) =
1104                (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(v))
1105            {
1106                response = response.header(name, value);
1107            }
1108        }
1109    }
1110    if !has_content_type {
1111        response = response.header("content-type", "application/json");
1112    }
1113
1114    Some(
1115        response
1116            .body(Body::from(body_bytes_out))
1117            .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
1118    )
1119}
1120
1121/// Axum fallback handler for the main router: tries to serve a dynamic mock,
1122/// or returns 404. Only invoked when nothing else in the router matched.
1123///
1124/// Wire via `.fallback_service(axum::routing::any(dynamic_mock_fallback).with_state(state))`
1125/// so the handler's own `State<ManagementState>` extractor is satisfied
1126/// without forcing the parent router to adopt `ManagementState` as its state.
1127pub async fn dynamic_mock_fallback(
1128    State(state): State<ManagementState>,
1129    req: Request<Body>,
1130) -> Response {
1131    match serve_dynamic_mock(&state, req).await {
1132        Some(resp) => resp,
1133        None => StatusCode::NOT_FOUND.into_response(),
1134    }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140
1141    #[tokio::test]
1142    async fn test_create_and_get_mock() {
1143        let state = ManagementState::new(None, None, 3000);
1144
1145        let mock = MockConfig {
1146            id: "test-1".to_string(),
1147            name: "Test Mock".to_string(),
1148            method: "GET".to_string(),
1149            path: "/test".to_string(),
1150            response: MockResponse {
1151                body: serde_json::json!({"message": "test"}),
1152                headers: None,
1153            },
1154            enabled: true,
1155            latency_ms: None,
1156            status_code: Some(200),
1157            request_match: None,
1158            priority: None,
1159            scenario: None,
1160            required_scenario_state: None,
1161            new_scenario_state: None,
1162        };
1163
1164        // Create mock
1165        {
1166            let mut mocks = state.mocks.write().await;
1167            mocks.push(mock.clone());
1168        }
1169
1170        // Get mock
1171        let mocks = state.mocks.read().await;
1172        let found = mocks.iter().find(|m| m.id == "test-1");
1173        assert!(found.is_some());
1174        assert_eq!(found.unwrap().name, "Test Mock");
1175    }
1176
1177    #[tokio::test]
1178    async fn test_server_stats() {
1179        let state = ManagementState::new(None, None, 3000);
1180
1181        // Add some mocks
1182        {
1183            let mut mocks = state.mocks.write().await;
1184            mocks.push(MockConfig {
1185                id: "1".to_string(),
1186                name: "Mock 1".to_string(),
1187                method: "GET".to_string(),
1188                path: "/test1".to_string(),
1189                response: MockResponse {
1190                    body: serde_json::json!({}),
1191                    headers: None,
1192                },
1193                enabled: true,
1194                latency_ms: None,
1195                status_code: Some(200),
1196                request_match: None,
1197                priority: None,
1198                scenario: None,
1199                required_scenario_state: None,
1200                new_scenario_state: None,
1201            });
1202            mocks.push(MockConfig {
1203                id: "2".to_string(),
1204                name: "Mock 2".to_string(),
1205                method: "POST".to_string(),
1206                path: "/test2".to_string(),
1207                response: MockResponse {
1208                    body: serde_json::json!({}),
1209                    headers: None,
1210                },
1211                enabled: false,
1212                latency_ms: None,
1213                status_code: Some(201),
1214                request_match: None,
1215                priority: None,
1216                scenario: None,
1217                required_scenario_state: None,
1218                new_scenario_state: None,
1219            });
1220        }
1221
1222        let mocks = state.mocks.read().await;
1223        assert_eq!(mocks.len(), 2);
1224        assert_eq!(mocks.iter().filter(|m| m.enabled).count(), 1);
1225    }
1226
1227    #[test]
1228    fn test_mock_matches_request_with_xpath_absolute_path() {
1229        let mock = MockConfig {
1230            id: "xpath-1".to_string(),
1231            name: "XPath Match".to_string(),
1232            method: "POST".to_string(),
1233            path: "/xml".to_string(),
1234            response: MockResponse {
1235                body: serde_json::json!({"ok": true}),
1236                headers: None,
1237            },
1238            enabled: true,
1239            latency_ms: None,
1240            status_code: Some(200),
1241            request_match: Some(RequestMatchCriteria {
1242                xpath: Some("/root/order/id".to_string()),
1243                ..Default::default()
1244            }),
1245            priority: None,
1246            scenario: None,
1247            required_scenario_state: None,
1248            new_scenario_state: None,
1249        };
1250
1251        let body = br#"<root><order><id>123</id></order></root>"#;
1252        let headers = std::collections::HashMap::new();
1253        let query = std::collections::HashMap::new();
1254
1255        assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1256    }
1257
1258    #[test]
1259    fn test_mock_matches_request_with_xpath_text_predicate() {
1260        let mock = MockConfig {
1261            id: "xpath-2".to_string(),
1262            name: "XPath Predicate Match".to_string(),
1263            method: "POST".to_string(),
1264            path: "/xml".to_string(),
1265            response: MockResponse {
1266                body: serde_json::json!({"ok": true}),
1267                headers: None,
1268            },
1269            enabled: true,
1270            latency_ms: None,
1271            status_code: Some(200),
1272            request_match: Some(RequestMatchCriteria {
1273                xpath: Some("//order/id[text()='123']".to_string()),
1274                ..Default::default()
1275            }),
1276            priority: None,
1277            scenario: None,
1278            required_scenario_state: None,
1279            new_scenario_state: None,
1280        };
1281
1282        let body = br#"<root><order><id>123</id></order></root>"#;
1283        let headers = std::collections::HashMap::new();
1284        let query = std::collections::HashMap::new();
1285
1286        assert!(mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1287    }
1288
1289    #[test]
1290    fn test_mock_matches_request_with_xpath_no_match() {
1291        let mock = MockConfig {
1292            id: "xpath-3".to_string(),
1293            name: "XPath No Match".to_string(),
1294            method: "POST".to_string(),
1295            path: "/xml".to_string(),
1296            response: MockResponse {
1297                body: serde_json::json!({"ok": true}),
1298                headers: None,
1299            },
1300            enabled: true,
1301            latency_ms: None,
1302            status_code: Some(200),
1303            request_match: Some(RequestMatchCriteria {
1304                xpath: Some("//order/id[text()='456']".to_string()),
1305                ..Default::default()
1306            }),
1307            priority: None,
1308            scenario: None,
1309            required_scenario_state: None,
1310            new_scenario_state: None,
1311        };
1312
1313        let body = br#"<root><order><id>123</id></order></root>"#;
1314        let headers = std::collections::HashMap::new();
1315        let query = std::collections::HashMap::new();
1316
1317        assert!(!mock_matches_request(&mock, "POST", "/xml", &headers, &query, Some(body)));
1318    }
1319}