Skip to main content

mockforge_core/protocol_abstraction/
mod.rs

1//! Protocol-agnostic abstractions for unified mocking across HTTP, GraphQL, gRPC, and WebSocket
2//!
3//! This module provides traits and types that abstract common patterns across different
4//! protocols, enabling code reuse for spec-driven mocking, middleware, and request matching.
5
6pub mod auth;
7pub mod base_registry;
8pub mod matcher;
9pub mod middleware;
10pub mod protocol_registry;
11pub mod streaming;
12
13use crate::Result;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::fmt;
17use std::sync::Arc;
18
19// Re-export middleware types
20pub use auth::{AuthMiddleware, AuthResult, Claims};
21pub use base_registry::{BaseSpecRegistry, ProtocolFixture};
22pub use matcher::{FuzzyRequestMatcher, RequestFingerprint, SimpleRequestMatcher};
23pub use middleware::{LatencyMiddleware, LoggingMiddleware, MetricsMiddleware};
24pub use protocol_registry::{ProtocolHandler, ProtocolRegistry};
25pub use streaming::{
26    MessageBuilder, MessageStream, ProtocolMessage, StreamingMetadata, StreamingProtocol,
27    StreamingProtocolRegistry,
28};
29
30// `Protocol` is re-exported from `mockforge_foundation::protocol` so both
31// core and `mockforge-contracts` share the canonical enum.
32pub use mockforge_foundation::protocol::Protocol;
33
34/// Message pattern enumeration for different communication patterns
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum MessagePattern {
37    /// Request-Response pattern (HTTP, gRPC unary)
38    RequestResponse,
39    /// One-way/fire-and-forget pattern (MQTT publish, email)
40    OneWay,
41    /// Publish-Subscribe pattern (Kafka, RabbitMQ, MQTT)
42    PubSub,
43    /// Streaming pattern (gRPC streaming, WebSocket)
44    Streaming,
45}
46
47impl fmt::Display for MessagePattern {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            MessagePattern::RequestResponse => write!(f, "Request-Response"),
51            MessagePattern::OneWay => write!(f, "One-Way"),
52            MessagePattern::PubSub => write!(f, "Pub-Sub"),
53            MessagePattern::Streaming => write!(f, "Streaming"),
54        }
55    }
56}
57
58/// A protocol-agnostic request representation
59#[derive(Debug, Clone)]
60pub struct ProtocolRequest {
61    /// The protocol this request uses
62    pub protocol: Protocol,
63    /// Message pattern for this request
64    pub pattern: MessagePattern,
65    /// Method or operation (e.g., "GET", "Query.users", "greeter.SayHello")
66    pub operation: String,
67    /// Path, query name, or service/method name
68    pub path: String,
69    /// Topic for pub/sub protocols (MQTT, Kafka)
70    pub topic: Option<String>,
71    /// Routing key for message queuing protocols (AMQP, RabbitMQ)
72    pub routing_key: Option<String>,
73    /// Partition for partitioned protocols (Kafka)
74    pub partition: Option<i32>,
75    /// Quality of Service level (MQTT: 0, 1, 2)
76    pub qos: Option<u8>,
77    /// Request metadata (headers, metadata, etc.)
78    pub metadata: HashMap<String, String>,
79    /// Request body/payload as bytes
80    pub body: Option<Vec<u8>>,
81    /// Client IP address if available
82    pub client_ip: Option<String>,
83}
84
85impl Default for ProtocolRequest {
86    fn default() -> Self {
87        Self {
88            protocol: Protocol::Http,
89            pattern: MessagePattern::RequestResponse,
90            operation: String::new(),
91            path: String::new(),
92            topic: None,
93            routing_key: None,
94            partition: None,
95            qos: None,
96            metadata: HashMap::new(),
97            body: None,
98            client_ip: None,
99        }
100    }
101}
102
103/// A protocol-agnostic response representation
104#[derive(Debug, Clone)]
105pub struct ProtocolResponse {
106    /// Status code or success indicator (HTTP: 200, gRPC: OK, GraphQL: no errors)
107    pub status: ResponseStatus,
108    /// Response metadata (headers, metadata, etc.)
109    pub metadata: HashMap<String, String>,
110    /// Response body/payload
111    pub body: Vec<u8>,
112    /// Content type or serialization format
113    pub content_type: String,
114}
115
116/// Response status abstraction across protocols
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum ResponseStatus {
119    /// HTTP status code
120    HttpStatus(u16),
121    /// gRPC status code
122    GrpcStatus(i32),
123    /// GraphQL success (true) or error (false)
124    GraphQLStatus(bool),
125    /// WebSocket status
126    WebSocketStatus(bool),
127    /// SMTP status code (2xx = success, 4xx/5xx = error)
128    SmtpStatus(u16),
129    /// MQTT status (true = success, false = error)
130    MqttStatus(bool),
131    /// Kafka status code (0 = success, non-zero = error)
132    KafkaStatus(i16),
133    /// AMQP/RabbitMQ status code
134    AmqpStatus(u16),
135    /// FTP status code
136    FtpStatus(u16),
137    /// TCP status (true = success, false = error/close)
138    TcpStatus(bool),
139}
140
141impl ResponseStatus {
142    /// Check if the response is successful
143    pub fn is_success(&self) -> bool {
144        match self {
145            ResponseStatus::HttpStatus(code) => (200..300).contains(code),
146            ResponseStatus::GrpcStatus(code) => *code == 0, // gRPC OK = 0
147            ResponseStatus::GraphQLStatus(success) => *success,
148            ResponseStatus::WebSocketStatus(success) => *success,
149            ResponseStatus::SmtpStatus(code) => (200..300).contains(code), // 2xx codes are success
150            ResponseStatus::MqttStatus(success) => *success,
151            ResponseStatus::KafkaStatus(code) => *code == 0, // Kafka OK = 0
152            ResponseStatus::AmqpStatus(code) => (200..300).contains(code), // AMQP success codes
153            ResponseStatus::FtpStatus(code) => (200..300).contains(code), // FTP success codes
154            ResponseStatus::TcpStatus(success) => *success,  // TCP success flag
155        }
156    }
157
158    /// Get numeric representation if applicable
159    pub fn as_code(&self) -> Option<i32> {
160        match self {
161            ResponseStatus::HttpStatus(code) => Some(*code as i32),
162            ResponseStatus::GrpcStatus(code) => Some(*code),
163            ResponseStatus::SmtpStatus(code) => Some(*code as i32),
164            ResponseStatus::KafkaStatus(code) => Some(*code as i32),
165            ResponseStatus::AmqpStatus(code) => Some(*code as i32),
166            ResponseStatus::FtpStatus(code) => Some(*code as i32),
167            ResponseStatus::TcpStatus(_) => None, // TCP uses boolean status
168            ResponseStatus::GraphQLStatus(_)
169            | ResponseStatus::WebSocketStatus(_)
170            | ResponseStatus::MqttStatus(_) => None,
171        }
172    }
173}
174
175/// Trait for spec-driven mocking registries (OpenAPI, GraphQL schema, Proto files)
176pub trait SpecRegistry: Send + Sync {
177    /// Get the protocol this registry handles
178    fn protocol(&self) -> Protocol;
179
180    /// Get all available operations/routes in this spec
181    fn operations(&self) -> Vec<SpecOperation>;
182
183    /// Find an operation by path/name
184    fn find_operation(&self, operation: &str, path: &str) -> Option<SpecOperation>;
185
186    /// Validate a request against the spec
187    fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult>;
188
189    /// Generate a mock response for a request
190    fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse>;
191}
192
193/// Represents a single operation in a spec (endpoint, query, RPC method)
194#[derive(Debug, Clone)]
195pub struct SpecOperation {
196    /// Operation name or identifier
197    pub name: String,
198    /// Path or fully qualified name
199    pub path: String,
200    /// Operation type (GET, POST, Query, Mutation, RPC)
201    pub operation_type: String,
202    /// Input schema/type information
203    pub input_schema: Option<String>,
204    /// Output schema/type information
205    pub output_schema: Option<String>,
206    /// Metadata from spec
207    pub metadata: HashMap<String, String>,
208}
209
210/// Result of request validation
211#[derive(Debug, Clone)]
212pub struct ValidationResult {
213    /// Whether validation passed
214    pub valid: bool,
215    /// Validation errors if any
216    pub errors: Vec<ValidationError>,
217    /// Validation warnings
218    pub warnings: Vec<String>,
219}
220
221/// A validation error
222#[derive(Debug, Clone)]
223pub struct ValidationError {
224    /// Error message
225    pub message: String,
226    /// Path to the error (e.g., "body.user.email")
227    pub path: Option<String>,
228    /// Error code
229    pub code: Option<String>,
230}
231
232impl ValidationResult {
233    /// Create a successful validation result
234    pub fn success() -> Self {
235        Self {
236            valid: true,
237            errors: Vec::new(),
238            warnings: Vec::new(),
239        }
240    }
241
242    /// Create a failed validation result with errors
243    pub fn failure(errors: Vec<ValidationError>) -> Self {
244        Self {
245            valid: false,
246            errors,
247            warnings: Vec::new(),
248        }
249    }
250
251    /// Add a warning to the result
252    pub fn with_warning(mut self, warning: String) -> Self {
253        self.warnings.push(warning);
254        self
255    }
256}
257
258/// Result of middleware request processing
259///
260/// Allows middleware to either continue the chain or short-circuit with a response
261/// (e.g., auth middleware rejecting an unauthenticated request with a 401).
262#[derive(Debug)]
263pub enum MiddlewareAction {
264    /// Continue processing the next middleware in the chain
265    Continue,
266    /// Stop the chain and return this response immediately
267    ShortCircuit(ProtocolResponse),
268}
269
270/// Trait for protocol-agnostic middleware
271#[async_trait::async_trait]
272pub trait ProtocolMiddleware: Send + Sync {
273    /// Get the name of this middleware
274    fn name(&self) -> &str;
275
276    /// Process a request before it reaches the handler
277    ///
278    /// Returns `MiddlewareAction::Continue` to pass the request to the next middleware,
279    /// or `MiddlewareAction::ShortCircuit(response)` to stop the chain and return early.
280    async fn process_request(&self, request: &mut ProtocolRequest) -> Result<MiddlewareAction>;
281
282    /// Process a response before it's returned to the client
283    async fn process_response(
284        &self,
285        request: &ProtocolRequest,
286        response: &mut ProtocolResponse,
287    ) -> Result<()>;
288
289    /// Check if this middleware should run for a given protocol
290    fn supports_protocol(&self, protocol: Protocol) -> bool;
291}
292
293/// Trait for request matching across protocols
294pub trait RequestMatcher: Send + Sync {
295    /// Match a request and return a score (higher = better match)
296    fn match_score(&self, request: &ProtocolRequest) -> f64;
297
298    /// Get the protocol this matcher handles
299    fn protocol(&self) -> Protocol;
300}
301
302/// Unified fixture format supporting all protocols
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct UnifiedFixture {
305    /// Unique identifier for this fixture
306    pub id: String,
307
308    /// Human-readable name
309    pub name: String,
310
311    /// Description of what this fixture does
312    #[serde(default)]
313    pub description: String,
314
315    /// Protocol this fixture applies to
316    pub protocol: Protocol,
317
318    /// Request matching criteria
319    pub request: FixtureRequest,
320
321    /// Response configuration
322    pub response: FixtureResponse,
323
324    /// Additional metadata
325    #[serde(default)]
326    pub metadata: HashMap<String, serde_json::Value>,
327
328    /// Whether this fixture is enabled
329    #[serde(default = "default_true")]
330    pub enabled: bool,
331
332    /// Priority for matching (higher = matched first)
333    #[serde(default)]
334    pub priority: i32,
335
336    /// Tags for organization
337    #[serde(default)]
338    pub tags: Vec<String>,
339}
340
341/// Request matching criteria for fixtures
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct FixtureRequest {
344    /// Message pattern to match
345    #[serde(default)]
346    pub pattern: Option<MessagePattern>,
347
348    /// Operation/method to match (exact or regex)
349    pub operation: Option<String>,
350
351    /// Path/route to match (exact or regex)
352    pub path: Option<String>,
353
354    /// Topic to match (for pub/sub protocols)
355    pub topic: Option<String>,
356
357    /// Routing key to match (for message queuing)
358    pub routing_key: Option<String>,
359
360    /// Partition to match
361    pub partition: Option<i32>,
362
363    /// QoS level to match
364    pub qos: Option<u8>,
365
366    /// Headers/metadata to match (key-value pairs)
367    #[serde(default)]
368    pub headers: HashMap<String, String>,
369
370    /// Request body pattern (regex for text, or exact match)
371    pub body_pattern: Option<String>,
372
373    /// Custom matching logic (script or expression)
374    pub custom_matcher: Option<String>,
375}
376
377/// Response configuration for fixtures
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct FixtureResponse {
380    /// Response status
381    pub status: FixtureStatus,
382
383    /// Response headers
384    #[serde(default)]
385    pub headers: HashMap<String, String>,
386
387    /// Response body (can be string, JSON, or base64-encoded binary)
388    pub body: Option<serde_json::Value>,
389
390    /// Content type
391    pub content_type: Option<String>,
392
393    /// Response delay in milliseconds
394    #[serde(default)]
395    pub delay_ms: u64,
396
397    /// Template variables for dynamic responses
398    #[serde(default)]
399    pub template_vars: HashMap<String, serde_json::Value>,
400}
401
402/// Status representation for fixtures
403#[derive(Debug, Clone, Serialize, Deserialize)]
404#[serde(untagged)]
405pub enum FixtureStatus {
406    /// HTTP status code
407    Http(u16),
408    /// gRPC status code
409    Grpc(i32),
410    /// Generic success/failure
411    Generic(bool),
412    /// Custom status with code and message
413    Custom {
414        /// Custom status code
415        code: i32,
416        /// Custom status message
417        message: String,
418    },
419}
420
421fn default_true() -> bool {
422    true
423}
424
425impl UnifiedFixture {
426    /// Check if this fixture matches the given protocol request
427    pub fn matches(&self, request: &ProtocolRequest) -> bool {
428        // Check protocol
429        if request.protocol != self.protocol {
430            return false;
431        }
432
433        // Check pattern
434        if let Some(pattern) = &self.request.pattern {
435            if request.pattern != *pattern {
436                return false;
437            }
438        }
439
440        // Check operation
441        if let Some(operation) = &self.request.operation {
442            if !self.matches_pattern(&request.operation, operation) {
443                return false;
444            }
445        }
446
447        // Check path
448        if let Some(path) = &self.request.path {
449            if !self.matches_pattern(&request.path, path) {
450                return false;
451            }
452        }
453
454        // Check topic
455        if let Some(topic) = &self.request.topic {
456            if !self.matches_pattern(request.topic.as_ref().unwrap_or(&String::new()), topic) {
457                return false;
458            }
459        }
460
461        // Check routing key
462        if let Some(routing_key) = &self.request.routing_key {
463            if !self.matches_pattern(
464                request.routing_key.as_ref().unwrap_or(&String::new()),
465                routing_key,
466            ) {
467                return false;
468            }
469        }
470
471        // Check partition
472        if let Some(partition) = self.request.partition {
473            if request.partition != Some(partition) {
474                return false;
475            }
476        }
477
478        // Check QoS
479        if let Some(qos) = self.request.qos {
480            if request.qos != Some(qos) {
481                return false;
482            }
483        }
484
485        // Check headers
486        for (key, expected_value) in &self.request.headers {
487            if let Some(actual_value) = request.metadata.get(key) {
488                if !self.matches_pattern(actual_value, expected_value) {
489                    return false;
490                }
491            } else {
492                return false;
493            }
494        }
495
496        // Check body pattern
497        if let Some(pattern) = &self.request.body_pattern {
498            if let Some(body) = &request.body {
499                let body_str = String::from_utf8_lossy(body);
500                if !self.matches_pattern(&body_str, pattern) {
501                    return false;
502                }
503            } else {
504                return false;
505            }
506        }
507
508        // Check custom matcher logic
509        if let Some(custom_matcher) = &self.request.custom_matcher {
510            if !self.evaluate_custom_matcher(custom_matcher, request) {
511                return false;
512            }
513        }
514
515        true
516    }
517
518    /// Helper method to match patterns (supports regex and exact match)
519    fn matches_pattern(&self, value: &str, pattern: &str) -> bool {
520        use regex::Regex;
521
522        // Try regex first
523        if let Ok(re) = Regex::new(pattern) {
524            re.is_match(value)
525        } else {
526            // Fall back to exact match
527            value == pattern
528        }
529    }
530
531    /// Evaluate custom matcher expression
532    fn evaluate_custom_matcher(&self, expression: &str, request: &ProtocolRequest) -> bool {
533        // Simple expression evaluator for custom matching logic
534        // Supports basic conditions like:
535        // - operation == "GET"
536        // - path =~ "/api/.*"
537        // - headers.content-type == "application/json"
538        // - body contains "test"
539
540        let expr = expression.trim();
541
542        // Handle different types of expressions
543        if expr.contains("==") {
544            self.evaluate_equality(expr, request)
545        } else if expr.contains("=~") {
546            self.evaluate_regex_match(expr, request)
547        } else if expr.contains("contains") {
548            self.evaluate_contains(expr, request)
549        } else {
550            // Unknown expression format, log warning and return false
551            tracing::warn!("Unknown custom matcher expression format: {}", expr);
552            false
553        }
554    }
555
556    /// Evaluate equality expressions (field == "value")
557    fn evaluate_equality(&self, expr: &str, request: &ProtocolRequest) -> bool {
558        let parts: Vec<&str> = expr.split("==").map(|s| s.trim()).collect();
559        if parts.len() != 2 {
560            return false;
561        }
562
563        let field = parts[0];
564        let expected_value = parts[1].trim_matches('"');
565
566        match field {
567            "operation" => request.operation == expected_value,
568            "path" => request.path == expected_value,
569            "topic" => request.topic.as_ref().unwrap_or(&String::new()) == expected_value,
570            "routing_key" => {
571                request.routing_key.as_ref().unwrap_or(&String::new()) == expected_value
572            }
573            _ if field.starts_with("headers.") => {
574                let header_name = &field[8..]; // Remove "headers." prefix
575                request.metadata.get(header_name).is_some_and(|v| v == expected_value)
576            }
577            _ => {
578                tracing::warn!("Unknown field in equality expression: {}", field);
579                false
580            }
581        }
582    }
583
584    /// Evaluate regex match expressions (field =~ "pattern")
585    fn evaluate_regex_match(&self, expr: &str, request: &ProtocolRequest) -> bool {
586        let parts: Vec<&str> = expr.split("=~").map(|s| s.trim()).collect();
587        if parts.len() != 2 {
588            return false;
589        }
590
591        let field = parts[0];
592        let pattern = parts[1].trim_matches('"');
593
594        let value: String = match field {
595            "operation" => request.operation.clone(),
596            "path" => request.path.clone(),
597            "topic" => request.topic.clone().unwrap_or_default(),
598            "routing_key" => request.routing_key.clone().unwrap_or_default(),
599            _ if field.starts_with("headers.") => {
600                let header_name = &field[8..]; // Remove "headers." prefix
601                request.metadata.get(header_name).cloned().unwrap_or_default()
602            }
603            _ => {
604                tracing::warn!("Unknown field in regex expression: {}", field);
605                return false;
606            }
607        };
608
609        use regex::Regex;
610        match Regex::new(pattern) {
611            Ok(re) => re.is_match(&value),
612            Err(e) => {
613                tracing::warn!("Invalid regex pattern '{}': {}", pattern, e);
614                false
615            }
616        }
617    }
618
619    /// Evaluate contains expressions (field contains "substring")
620    fn evaluate_contains(&self, expr: &str, request: &ProtocolRequest) -> bool {
621        let parts: Vec<&str> = expr.split("contains").map(|s| s.trim()).collect();
622        if parts.len() != 2 {
623            return false;
624        }
625
626        let field = parts[0];
627        let substring = parts[1].trim_matches('"');
628
629        let value: String = match field {
630            "body" => {
631                if let Some(body) = &request.body {
632                    String::from_utf8_lossy(body).to_string()
633                } else {
634                    return false;
635                }
636            }
637            _ if field.starts_with("headers.") => {
638                let header_name = &field[8..]; // Remove "headers." prefix
639                request.metadata.get(header_name).cloned().unwrap_or_default()
640            }
641            _ => {
642                tracing::warn!("Unsupported field for contains expression: {}", field);
643                return false;
644            }
645        };
646
647        value.contains(substring)
648    }
649
650    /// Convert fixture response to ProtocolResponse
651    pub fn to_protocol_response(&self) -> Result<ProtocolResponse> {
652        let status = match &self.response.status {
653            FixtureStatus::Http(code) => ResponseStatus::HttpStatus(*code),
654            FixtureStatus::Grpc(code) => ResponseStatus::GrpcStatus(*code),
655            FixtureStatus::Generic(success) => ResponseStatus::GraphQLStatus(*success), // Using GraphQL as generic
656            FixtureStatus::Custom { code, .. } => ResponseStatus::GrpcStatus(*code), // Using gRPC as custom
657        };
658
659        let body = match &self.response.body {
660            Some(serde_json::Value::String(s)) => s.clone().into_bytes(),
661            Some(value) => serde_json::to_string(value)?.into_bytes(),
662            None => Vec::new(),
663        };
664
665        let content_type = self
666            .response
667            .content_type
668            .clone()
669            .unwrap_or_else(|| "application/json".to_string());
670
671        Ok(ProtocolResponse {
672            status,
673            metadata: self.response.headers.clone(),
674            body,
675            content_type,
676        })
677    }
678}
679
680/// Middleware chain for composing and executing multiple middleware in sequence
681pub struct MiddlewareChain {
682    /// Ordered list of middleware to execute
683    middleware: Vec<Arc<dyn ProtocolMiddleware>>,
684}
685
686impl MiddlewareChain {
687    /// Create a new middleware chain
688    pub fn new() -> Self {
689        Self {
690            middleware: Vec::new(),
691        }
692    }
693
694    /// Add middleware to the chain
695    pub fn with_middleware(mut self, middleware: Arc<dyn ProtocolMiddleware>) -> Self {
696        self.middleware.push(middleware);
697        self
698    }
699
700    /// Process a request through all middleware
701    ///
702    /// Returns `Ok(None)` if all middleware passed (continue to handler),
703    /// or `Ok(Some(response))` if a middleware short-circuited the chain.
704    pub async fn process_request(
705        &self,
706        request: &mut ProtocolRequest,
707    ) -> Result<Option<ProtocolResponse>> {
708        for middleware in &self.middleware {
709            if middleware.supports_protocol(request.protocol) {
710                match middleware.process_request(request).await? {
711                    MiddlewareAction::Continue => {}
712                    MiddlewareAction::ShortCircuit(response) => {
713                        tracing::debug!(
714                            middleware = middleware.name(),
715                            "Middleware short-circuited request processing"
716                        );
717                        return Ok(Some(response));
718                    }
719                }
720            }
721        }
722        Ok(None)
723    }
724
725    /// Process a response through all middleware (in reverse order)
726    pub async fn process_response(
727        &self,
728        request: &ProtocolRequest,
729        response: &mut ProtocolResponse,
730    ) -> Result<()> {
731        for middleware in self.middleware.iter().rev() {
732            if middleware.supports_protocol(request.protocol) {
733                middleware.process_response(request, response).await?;
734            }
735        }
736        Ok(())
737    }
738}
739
740impl Default for MiddlewareChain {
741    fn default() -> Self {
742        Self::new()
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749
750    #[test]
751    fn test_protocol_display() {
752        assert_eq!(Protocol::Http.to_string(), "HTTP");
753        assert_eq!(Protocol::GraphQL.to_string(), "GraphQL");
754        assert_eq!(Protocol::Grpc.to_string(), "gRPC");
755        assert_eq!(Protocol::WebSocket.to_string(), "WebSocket");
756        assert_eq!(Protocol::Smtp.to_string(), "SMTP");
757        assert_eq!(Protocol::Mqtt.to_string(), "MQTT");
758        assert_eq!(Protocol::Ftp.to_string(), "FTP");
759        assert_eq!(Protocol::Kafka.to_string(), "Kafka");
760        assert_eq!(Protocol::RabbitMq.to_string(), "RabbitMQ");
761        assert_eq!(Protocol::Amqp.to_string(), "AMQP");
762        assert_eq!(Protocol::Tcp.to_string(), "TCP");
763    }
764
765    #[test]
766    fn test_response_status_is_success() {
767        assert!(ResponseStatus::HttpStatus(200).is_success());
768        assert!(ResponseStatus::HttpStatus(204).is_success());
769        assert!(!ResponseStatus::HttpStatus(404).is_success());
770        assert!(!ResponseStatus::HttpStatus(500).is_success());
771
772        assert!(ResponseStatus::GrpcStatus(0).is_success());
773        assert!(!ResponseStatus::GrpcStatus(2).is_success());
774
775        assert!(ResponseStatus::GraphQLStatus(true).is_success());
776        assert!(!ResponseStatus::GraphQLStatus(false).is_success());
777    }
778
779    #[test]
780    fn test_response_status_as_code() {
781        assert_eq!(ResponseStatus::HttpStatus(200).as_code(), Some(200));
782        assert_eq!(ResponseStatus::GrpcStatus(0).as_code(), Some(0));
783        assert_eq!(ResponseStatus::GraphQLStatus(true).as_code(), None);
784    }
785
786    #[test]
787    fn test_validation_result_success() {
788        let result = ValidationResult::success();
789        assert!(result.valid);
790        assert_eq!(result.errors.len(), 0);
791        assert_eq!(result.warnings.len(), 0);
792    }
793
794    #[test]
795    fn test_validation_result_failure() {
796        let errors = vec![ValidationError {
797            message: "Invalid field".to_string(),
798            path: Some("body.field".to_string()),
799            code: Some("INVALID_FIELD".to_string()),
800        }];
801        let result = ValidationResult::failure(errors);
802        assert!(!result.valid);
803        assert_eq!(result.errors.len(), 1);
804    }
805
806    #[test]
807    fn test_validation_result_with_warning() {
808        let result = ValidationResult::success().with_warning("Deprecated field used".to_string());
809        assert!(result.valid);
810        assert_eq!(result.warnings.len(), 1);
811    }
812
813    #[test]
814    fn test_middleware_chain_creation() {
815        let chain = MiddlewareChain::new();
816        assert_eq!(chain.middleware.len(), 0);
817    }
818
819    #[test]
820    fn test_protocol_request_creation() {
821        let request = ProtocolRequest {
822            protocol: Protocol::Http,
823            operation: "GET".to_string(),
824            path: "/users".to_string(),
825            client_ip: Some("127.0.0.1".to_string()),
826            ..Default::default()
827        };
828        assert_eq!(request.protocol, Protocol::Http);
829        assert_eq!(request.pattern, MessagePattern::RequestResponse);
830        assert_eq!(request.operation, "GET");
831        assert_eq!(request.path, "/users");
832    }
833
834    #[test]
835    fn test_protocol_response_creation() {
836        let response = ProtocolResponse {
837            status: ResponseStatus::HttpStatus(200),
838            metadata: HashMap::new(),
839            body: b"{}".to_vec(),
840            content_type: "application/json".to_string(),
841        };
842        assert!(response.status.is_success());
843        assert_eq!(response.content_type, "application/json");
844    }
845
846    #[test]
847    fn test_unified_fixture_matching() {
848        let fixture = UnifiedFixture {
849            id: "test-fixture".to_string(),
850            name: "Test Fixture".to_string(),
851            description: "A test fixture".to_string(),
852            protocol: Protocol::Http,
853            request: FixtureRequest {
854                pattern: Some(MessagePattern::RequestResponse),
855                operation: Some("GET".to_string()),
856                path: Some("/api/users".to_string()),
857                topic: None,
858                routing_key: None,
859                partition: None,
860                qos: None,
861                headers: HashMap::new(),
862                body_pattern: None,
863                custom_matcher: None,
864            },
865            response: FixtureResponse {
866                status: FixtureStatus::Http(200),
867                headers: HashMap::new(),
868                body: Some(serde_json::json!({"users": ["john", "jane"]})),
869                content_type: Some("application/json".to_string()),
870                delay_ms: 0,
871                template_vars: HashMap::new(),
872            },
873            metadata: HashMap::new(),
874            enabled: true,
875            priority: 0,
876            tags: vec![],
877        };
878
879        let matching_request = ProtocolRequest {
880            protocol: Protocol::Http,
881            pattern: MessagePattern::RequestResponse,
882            operation: "GET".to_string(),
883            path: "/api/users".to_string(),
884            topic: None,
885            routing_key: None,
886            partition: None,
887            qos: None,
888            metadata: HashMap::new(),
889            body: None,
890            client_ip: None,
891        };
892
893        let non_matching_request = ProtocolRequest {
894            protocol: Protocol::Http,
895            pattern: MessagePattern::RequestResponse,
896            operation: "POST".to_string(),
897            path: "/api/users".to_string(),
898            topic: None,
899            routing_key: None,
900            partition: None,
901            qos: None,
902            metadata: HashMap::new(),
903            body: None,
904            client_ip: None,
905        };
906
907        assert!(fixture.matches(&matching_request));
908        assert!(!fixture.matches(&non_matching_request));
909    }
910
911    #[test]
912    fn test_fixture_to_protocol_response() {
913        let fixture = UnifiedFixture {
914            id: "test".to_string(),
915            name: "Test".to_string(),
916            description: "".to_string(),
917            protocol: Protocol::Http,
918            request: FixtureRequest {
919                pattern: None,
920                operation: None,
921                path: None,
922                topic: None,
923                routing_key: None,
924                partition: None,
925                qos: None,
926                headers: HashMap::new(),
927                body_pattern: None,
928                custom_matcher: None,
929            },
930            response: FixtureResponse {
931                status: FixtureStatus::Http(200),
932                headers: {
933                    let mut h = HashMap::new();
934                    h.insert("content-type".to_string(), "application/json".to_string());
935                    h
936                },
937                body: Some(serde_json::json!({"message": "ok"})),
938                content_type: Some("application/json".to_string()),
939                delay_ms: 0,
940                template_vars: HashMap::new(),
941            },
942            metadata: HashMap::new(),
943            enabled: true,
944            priority: 0,
945            tags: vec![],
946        };
947
948        let response = fixture.to_protocol_response().unwrap();
949        assert!(response.status.is_success());
950        assert_eq!(response.content_type, "application/json");
951        assert_eq!(response.metadata.get("content-type"), Some(&"application/json".to_string()));
952    }
953
954    #[test]
955    fn test_fixture_status_serialization() {
956        // Test HTTP status
957        let status = FixtureStatus::Http(404);
958        let serialized = serde_json::to_string(&status).unwrap();
959        assert_eq!(serialized, "404");
960
961        // Test gRPC status
962        let status = FixtureStatus::Grpc(5);
963        let serialized = serde_json::to_string(&status).unwrap();
964        assert_eq!(serialized, "5");
965
966        // Test generic status
967        let status = FixtureStatus::Generic(true);
968        let serialized = serde_json::to_string(&status).unwrap();
969        assert_eq!(serialized, "true");
970
971        // Test custom status
972        let status = FixtureStatus::Custom {
973            code: 500,
974            message: "Internal Error".to_string(),
975        };
976        let serialized = serde_json::to_string(&status).unwrap();
977        let expected: serde_json::Value =
978            serde_json::json!({"code": 500, "message": "Internal Error"});
979        assert_eq!(serde_json::from_str::<serde_json::Value>(&serialized).unwrap(), expected);
980    }
981
982    #[test]
983    fn test_fixture_pattern_matching() {
984        let fixture = UnifiedFixture {
985            id: "test".to_string(),
986            name: "Test".to_string(),
987            description: "".to_string(),
988            protocol: Protocol::Http,
989            request: FixtureRequest {
990                pattern: Some(MessagePattern::RequestResponse),
991                operation: Some("GET".to_string()),
992                path: Some("/api/.*".to_string()),
993                topic: None,
994                routing_key: None,
995                partition: None,
996                qos: None,
997                headers: HashMap::new(),
998                body_pattern: None,
999                custom_matcher: None,
1000            },
1001            response: FixtureResponse {
1002                status: FixtureStatus::Http(200),
1003                headers: HashMap::new(),
1004                body: None,
1005                content_type: None,
1006                delay_ms: 0,
1007                template_vars: HashMap::new(),
1008            },
1009            metadata: HashMap::new(),
1010            enabled: true,
1011            priority: 0,
1012            tags: vec![],
1013        };
1014
1015        // Test matching request
1016        let request = ProtocolRequest {
1017            protocol: Protocol::Http,
1018            pattern: MessagePattern::RequestResponse,
1019            operation: "GET".to_string(),
1020            path: "/api/users".to_string(),
1021            topic: None,
1022            routing_key: None,
1023            partition: None,
1024            qos: None,
1025            metadata: HashMap::new(),
1026            body: None,
1027            client_ip: None,
1028        };
1029        assert!(fixture.matches(&request));
1030
1031        // Test non-matching protocol
1032        let grpc_request = ProtocolRequest {
1033            protocol: Protocol::Grpc,
1034            pattern: MessagePattern::RequestResponse,
1035            operation: "GET".to_string(),
1036            path: "/api/users".to_string(),
1037            topic: None,
1038            routing_key: None,
1039            partition: None,
1040            qos: None,
1041            metadata: HashMap::new(),
1042            body: None,
1043            client_ip: None,
1044        };
1045        assert!(!fixture.matches(&grpc_request));
1046
1047        // Test non-matching operation
1048        let post_request = ProtocolRequest {
1049            protocol: Protocol::Http,
1050            pattern: MessagePattern::RequestResponse,
1051            operation: "POST".to_string(),
1052            path: "/api/users".to_string(),
1053            topic: None,
1054            routing_key: None,
1055            partition: None,
1056            qos: None,
1057            metadata: HashMap::new(),
1058            body: None,
1059            client_ip: None,
1060        };
1061        assert!(!fixture.matches(&post_request));
1062    }
1063
1064    #[test]
1065    fn test_custom_matcher_equality() {
1066        let fixture = UnifiedFixture {
1067            id: "test".to_string(),
1068            name: "Test".to_string(),
1069            description: "".to_string(),
1070            protocol: Protocol::Http,
1071            request: FixtureRequest {
1072                pattern: Some(MessagePattern::RequestResponse),
1073                operation: Some("GET".to_string()),
1074                path: Some("/api/users".to_string()),
1075                topic: None,
1076                routing_key: None,
1077                partition: None,
1078                qos: None,
1079                headers: HashMap::new(),
1080                body_pattern: None,
1081                custom_matcher: Some("operation == \"GET\"".to_string()),
1082            },
1083            response: FixtureResponse {
1084                status: FixtureStatus::Http(200),
1085                headers: HashMap::new(),
1086                body: None,
1087                content_type: None,
1088                delay_ms: 0,
1089                template_vars: HashMap::new(),
1090            },
1091            metadata: HashMap::new(),
1092            enabled: true,
1093            priority: 0,
1094            tags: vec![],
1095        };
1096
1097        // Test matching request
1098        let request = ProtocolRequest {
1099            protocol: Protocol::Http,
1100            pattern: MessagePattern::RequestResponse,
1101            operation: "GET".to_string(),
1102            path: "/api/users".to_string(),
1103            topic: None,
1104            routing_key: None,
1105            partition: None,
1106            qos: None,
1107            metadata: HashMap::new(),
1108            body: None,
1109            client_ip: None,
1110        };
1111        assert!(fixture.matches(&request));
1112
1113        // Test non-matching request
1114        let post_request = ProtocolRequest {
1115            protocol: Protocol::Http,
1116            pattern: MessagePattern::RequestResponse,
1117            operation: "POST".to_string(),
1118            path: "/api/users".to_string(),
1119            topic: None,
1120            routing_key: None,
1121            partition: None,
1122            qos: None,
1123            metadata: HashMap::new(),
1124            body: None,
1125            client_ip: None,
1126        };
1127        assert!(!fixture.matches(&post_request));
1128    }
1129
1130    #[test]
1131    fn test_custom_matcher_regex() {
1132        let fixture = UnifiedFixture {
1133            id: "test".to_string(),
1134            name: "Test".to_string(),
1135            description: "".to_string(),
1136            protocol: Protocol::Http,
1137            request: FixtureRequest {
1138                pattern: Some(MessagePattern::RequestResponse),
1139                operation: Some("GET".to_string()),
1140                path: Some("/api/.*".to_string()),
1141                topic: None,
1142                routing_key: None,
1143                partition: None,
1144                qos: None,
1145                headers: HashMap::new(),
1146                body_pattern: None,
1147                custom_matcher: Some("path =~ \"/api/.*\"".to_string()),
1148            },
1149            response: FixtureResponse {
1150                status: FixtureStatus::Http(200),
1151                headers: HashMap::new(),
1152                body: None,
1153                content_type: None,
1154                delay_ms: 0,
1155                template_vars: HashMap::new(),
1156            },
1157            metadata: HashMap::new(),
1158            enabled: true,
1159            priority: 0,
1160            tags: vec![],
1161        };
1162
1163        // Test matching request
1164        let request = ProtocolRequest {
1165            protocol: Protocol::Http,
1166            pattern: MessagePattern::RequestResponse,
1167            operation: "GET".to_string(),
1168            path: "/api/users".to_string(),
1169            topic: None,
1170            routing_key: None,
1171            partition: None,
1172            qos: None,
1173            metadata: HashMap::new(),
1174            body: None,
1175            client_ip: None,
1176        };
1177        assert!(fixture.matches(&request));
1178
1179        // Test non-matching request
1180        let other_request = ProtocolRequest {
1181            protocol: Protocol::Http,
1182            pattern: MessagePattern::RequestResponse,
1183            operation: "GET".to_string(),
1184            path: "/other/path".to_string(),
1185            topic: None,
1186            routing_key: None,
1187            partition: None,
1188            qos: None,
1189            metadata: HashMap::new(),
1190            body: None,
1191            client_ip: None,
1192        };
1193        assert!(!fixture.matches(&other_request));
1194    }
1195
1196    #[test]
1197    fn test_custom_matcher_contains() {
1198        let fixture = UnifiedFixture {
1199            id: "test".to_string(),
1200            name: "Test".to_string(),
1201            description: "".to_string(),
1202            protocol: Protocol::Http,
1203            request: FixtureRequest {
1204                pattern: Some(MessagePattern::RequestResponse),
1205                operation: Some("POST".to_string()),
1206                path: Some("/api/users".to_string()),
1207                topic: None,
1208                routing_key: None,
1209                partition: None,
1210                qos: None,
1211                headers: HashMap::new(),
1212                body_pattern: None,
1213                custom_matcher: Some("body contains \"test\"".to_string()),
1214            },
1215            response: FixtureResponse {
1216                status: FixtureStatus::Http(200),
1217                headers: HashMap::new(),
1218                body: None,
1219                content_type: None,
1220                delay_ms: 0,
1221                template_vars: HashMap::new(),
1222            },
1223            metadata: HashMap::new(),
1224            enabled: true,
1225            priority: 0,
1226            tags: vec![],
1227        };
1228
1229        // Test matching request
1230        let request = ProtocolRequest {
1231            protocol: Protocol::Http,
1232            pattern: MessagePattern::RequestResponse,
1233            operation: "POST".to_string(),
1234            path: "/api/users".to_string(),
1235            topic: None,
1236            routing_key: None,
1237            partition: None,
1238            qos: None,
1239            metadata: HashMap::new(),
1240            body: Some(b"{\"name\": \"test user\"}".to_vec()),
1241            client_ip: None,
1242        };
1243        assert!(fixture.matches(&request));
1244
1245        // Test non-matching request
1246        let no_match_request = ProtocolRequest {
1247            protocol: Protocol::Http,
1248            pattern: MessagePattern::RequestResponse,
1249            operation: "POST".to_string(),
1250            path: "/api/users".to_string(),
1251            topic: None,
1252            routing_key: None,
1253            partition: None,
1254            qos: None,
1255            metadata: HashMap::new(),
1256            body: Some(b"{\"name\": \"other user\"}".to_vec()),
1257            client_ip: None,
1258        };
1259        assert!(!fixture.matches(&no_match_request));
1260    }
1261
1262    #[test]
1263    fn test_custom_matcher_header() {
1264        let fixture = UnifiedFixture {
1265            id: "test".to_string(),
1266            name: "Test".to_string(),
1267            description: "".to_string(),
1268            protocol: Protocol::Http,
1269            request: FixtureRequest {
1270                pattern: Some(MessagePattern::RequestResponse),
1271                operation: Some("GET".to_string()),
1272                path: Some("/api/data".to_string()),
1273                topic: None,
1274                routing_key: None,
1275                partition: None,
1276                qos: None,
1277                headers: HashMap::new(),
1278                body_pattern: None,
1279                custom_matcher: Some("headers.content-type == \"application/json\"".to_string()),
1280            },
1281            response: FixtureResponse {
1282                status: FixtureStatus::Http(200),
1283                headers: HashMap::new(),
1284                body: None,
1285                content_type: None,
1286                delay_ms: 0,
1287                template_vars: HashMap::new(),
1288            },
1289            metadata: HashMap::new(),
1290            enabled: true,
1291            priority: 0,
1292            tags: vec![],
1293        };
1294
1295        // Test matching request
1296        let mut headers = HashMap::new();
1297        headers.insert("content-type".to_string(), "application/json".to_string());
1298        let request = ProtocolRequest {
1299            protocol: Protocol::Http,
1300            pattern: MessagePattern::RequestResponse,
1301            operation: "GET".to_string(),
1302            path: "/api/data".to_string(),
1303            topic: None,
1304            routing_key: None,
1305            partition: None,
1306            qos: None,
1307            metadata: headers,
1308            body: None,
1309            client_ip: None,
1310        };
1311        assert!(fixture.matches(&request));
1312
1313        // Test non-matching request
1314        let mut wrong_headers = HashMap::new();
1315        wrong_headers.insert("content-type".to_string(), "text/plain".to_string());
1316        let wrong_request = ProtocolRequest {
1317            protocol: Protocol::Http,
1318            pattern: MessagePattern::RequestResponse,
1319            operation: "GET".to_string(),
1320            path: "/api/data".to_string(),
1321            topic: None,
1322            routing_key: None,
1323            partition: None,
1324            qos: None,
1325            metadata: wrong_headers,
1326            body: None,
1327            client_ip: None,
1328        };
1329        assert!(!fixture.matches(&wrong_request));
1330    }
1331}