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