1pub 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
19pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33pub enum Protocol {
34 Http,
36 GraphQL,
38 Grpc,
40 WebSocket,
42 Smtp,
44 Mqtt,
46 Ftp,
48 Kafka,
50 RabbitMq,
52 Amqp,
54 Tcp,
56}
57
58impl fmt::Display for Protocol {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Protocol::Http => write!(f, "HTTP"),
62 Protocol::GraphQL => write!(f, "GraphQL"),
63 Protocol::Grpc => write!(f, "gRPC"),
64 Protocol::WebSocket => write!(f, "WebSocket"),
65 Protocol::Smtp => write!(f, "SMTP"),
66 Protocol::Mqtt => write!(f, "MQTT"),
67 Protocol::Ftp => write!(f, "FTP"),
68 Protocol::Kafka => write!(f, "Kafka"),
69 Protocol::RabbitMq => write!(f, "RabbitMQ"),
70 Protocol::Amqp => write!(f, "AMQP"),
71 Protocol::Tcp => write!(f, "TCP"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum MessagePattern {
79 RequestResponse,
81 OneWay,
83 PubSub,
85 Streaming,
87}
88
89impl fmt::Display for MessagePattern {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 MessagePattern::RequestResponse => write!(f, "Request-Response"),
93 MessagePattern::OneWay => write!(f, "One-Way"),
94 MessagePattern::PubSub => write!(f, "Pub-Sub"),
95 MessagePattern::Streaming => write!(f, "Streaming"),
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct ProtocolRequest {
103 pub protocol: Protocol,
105 pub pattern: MessagePattern,
107 pub operation: String,
109 pub path: String,
111 pub topic: Option<String>,
113 pub routing_key: Option<String>,
115 pub partition: Option<i32>,
117 pub qos: Option<u8>,
119 pub metadata: HashMap<String, String>,
121 pub body: Option<Vec<u8>>,
123 pub client_ip: Option<String>,
125}
126
127impl Default for ProtocolRequest {
128 fn default() -> Self {
129 Self {
130 protocol: Protocol::Http,
131 pattern: MessagePattern::RequestResponse,
132 operation: String::new(),
133 path: String::new(),
134 topic: None,
135 routing_key: None,
136 partition: None,
137 qos: None,
138 metadata: HashMap::new(),
139 body: None,
140 client_ip: None,
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct ProtocolResponse {
148 pub status: ResponseStatus,
150 pub metadata: HashMap<String, String>,
152 pub body: Vec<u8>,
154 pub content_type: String,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum ResponseStatus {
161 HttpStatus(u16),
163 GrpcStatus(i32),
165 GraphQLStatus(bool),
167 WebSocketStatus(bool),
169 SmtpStatus(u16),
171 MqttStatus(bool),
173 KafkaStatus(i16),
175 AmqpStatus(u16),
177 FtpStatus(u16),
179 TcpStatus(bool),
181}
182
183impl ResponseStatus {
184 pub fn is_success(&self) -> bool {
186 match self {
187 ResponseStatus::HttpStatus(code) => (200..300).contains(code),
188 ResponseStatus::GrpcStatus(code) => *code == 0, ResponseStatus::GraphQLStatus(success) => *success,
190 ResponseStatus::WebSocketStatus(success) => *success,
191 ResponseStatus::SmtpStatus(code) => (200..300).contains(code), ResponseStatus::MqttStatus(success) => *success,
193 ResponseStatus::KafkaStatus(code) => *code == 0, ResponseStatus::AmqpStatus(code) => (200..300).contains(code), ResponseStatus::FtpStatus(code) => (200..300).contains(code), ResponseStatus::TcpStatus(success) => *success, }
198 }
199
200 pub fn as_code(&self) -> Option<i32> {
202 match self {
203 ResponseStatus::HttpStatus(code) => Some(*code as i32),
204 ResponseStatus::GrpcStatus(code) => Some(*code),
205 ResponseStatus::SmtpStatus(code) => Some(*code as i32),
206 ResponseStatus::KafkaStatus(code) => Some(*code as i32),
207 ResponseStatus::AmqpStatus(code) => Some(*code as i32),
208 ResponseStatus::FtpStatus(code) => Some(*code as i32),
209 ResponseStatus::TcpStatus(_) => None, ResponseStatus::GraphQLStatus(_)
211 | ResponseStatus::WebSocketStatus(_)
212 | ResponseStatus::MqttStatus(_) => None,
213 }
214 }
215}
216
217pub trait SpecRegistry: Send + Sync {
219 fn protocol(&self) -> Protocol;
221
222 fn operations(&self) -> Vec<SpecOperation>;
224
225 fn find_operation(&self, operation: &str, path: &str) -> Option<SpecOperation>;
227
228 fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult>;
230
231 fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse>;
233}
234
235#[derive(Debug, Clone)]
237pub struct SpecOperation {
238 pub name: String,
240 pub path: String,
242 pub operation_type: String,
244 pub input_schema: Option<String>,
246 pub output_schema: Option<String>,
248 pub metadata: HashMap<String, String>,
250}
251
252#[derive(Debug, Clone)]
254pub struct ValidationResult {
255 pub valid: bool,
257 pub errors: Vec<ValidationError>,
259 pub warnings: Vec<String>,
261}
262
263#[derive(Debug, Clone)]
265pub struct ValidationError {
266 pub message: String,
268 pub path: Option<String>,
270 pub code: Option<String>,
272}
273
274impl ValidationResult {
275 pub fn success() -> Self {
277 Self {
278 valid: true,
279 errors: Vec::new(),
280 warnings: Vec::new(),
281 }
282 }
283
284 pub fn failure(errors: Vec<ValidationError>) -> Self {
286 Self {
287 valid: false,
288 errors,
289 warnings: Vec::new(),
290 }
291 }
292
293 pub fn with_warning(mut self, warning: String) -> Self {
295 self.warnings.push(warning);
296 self
297 }
298}
299
300#[derive(Debug)]
305pub enum MiddlewareAction {
306 Continue,
308 ShortCircuit(ProtocolResponse),
310}
311
312#[async_trait::async_trait]
314pub trait ProtocolMiddleware: Send + Sync {
315 fn name(&self) -> &str;
317
318 async fn process_request(&self, request: &mut ProtocolRequest) -> Result<MiddlewareAction>;
323
324 async fn process_response(
326 &self,
327 request: &ProtocolRequest,
328 response: &mut ProtocolResponse,
329 ) -> Result<()>;
330
331 fn supports_protocol(&self, protocol: Protocol) -> bool;
333}
334
335pub trait RequestMatcher: Send + Sync {
337 fn match_score(&self, request: &ProtocolRequest) -> f64;
339
340 fn protocol(&self) -> Protocol;
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct UnifiedFixture {
347 pub id: String,
349
350 pub name: String,
352
353 #[serde(default)]
355 pub description: String,
356
357 pub protocol: Protocol,
359
360 pub request: FixtureRequest,
362
363 pub response: FixtureResponse,
365
366 #[serde(default)]
368 pub metadata: HashMap<String, serde_json::Value>,
369
370 #[serde(default = "default_true")]
372 pub enabled: bool,
373
374 #[serde(default)]
376 pub priority: i32,
377
378 #[serde(default)]
380 pub tags: Vec<String>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct FixtureRequest {
386 #[serde(default)]
388 pub pattern: Option<MessagePattern>,
389
390 pub operation: Option<String>,
392
393 pub path: Option<String>,
395
396 pub topic: Option<String>,
398
399 pub routing_key: Option<String>,
401
402 pub partition: Option<i32>,
404
405 pub qos: Option<u8>,
407
408 #[serde(default)]
410 pub headers: HashMap<String, String>,
411
412 pub body_pattern: Option<String>,
414
415 pub custom_matcher: Option<String>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct FixtureResponse {
422 pub status: FixtureStatus,
424
425 #[serde(default)]
427 pub headers: HashMap<String, String>,
428
429 pub body: Option<serde_json::Value>,
431
432 pub content_type: Option<String>,
434
435 #[serde(default)]
437 pub delay_ms: u64,
438
439 #[serde(default)]
441 pub template_vars: HashMap<String, serde_json::Value>,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
446#[serde(untagged)]
447pub enum FixtureStatus {
448 Http(u16),
450 Grpc(i32),
452 Generic(bool),
454 Custom {
456 code: i32,
458 message: String,
460 },
461}
462
463fn default_true() -> bool {
464 true
465}
466
467impl UnifiedFixture {
468 pub fn matches(&self, request: &ProtocolRequest) -> bool {
470 if request.protocol != self.protocol {
472 return false;
473 }
474
475 if let Some(pattern) = &self.request.pattern {
477 if request.pattern != *pattern {
478 return false;
479 }
480 }
481
482 if let Some(operation) = &self.request.operation {
484 if !self.matches_pattern(&request.operation, operation) {
485 return false;
486 }
487 }
488
489 if let Some(path) = &self.request.path {
491 if !self.matches_pattern(&request.path, path) {
492 return false;
493 }
494 }
495
496 if let Some(topic) = &self.request.topic {
498 if !self.matches_pattern(request.topic.as_ref().unwrap_or(&String::new()), topic) {
499 return false;
500 }
501 }
502
503 if let Some(routing_key) = &self.request.routing_key {
505 if !self.matches_pattern(
506 request.routing_key.as_ref().unwrap_or(&String::new()),
507 routing_key,
508 ) {
509 return false;
510 }
511 }
512
513 if let Some(partition) = self.request.partition {
515 if request.partition != Some(partition) {
516 return false;
517 }
518 }
519
520 if let Some(qos) = self.request.qos {
522 if request.qos != Some(qos) {
523 return false;
524 }
525 }
526
527 for (key, expected_value) in &self.request.headers {
529 if let Some(actual_value) = request.metadata.get(key) {
530 if !self.matches_pattern(actual_value, expected_value) {
531 return false;
532 }
533 } else {
534 return false;
535 }
536 }
537
538 if let Some(pattern) = &self.request.body_pattern {
540 if let Some(body) = &request.body {
541 let body_str = String::from_utf8_lossy(body);
542 if !self.matches_pattern(&body_str, pattern) {
543 return false;
544 }
545 } else {
546 return false;
547 }
548 }
549
550 if let Some(custom_matcher) = &self.request.custom_matcher {
552 if !self.evaluate_custom_matcher(custom_matcher, request) {
553 return false;
554 }
555 }
556
557 true
558 }
559
560 fn matches_pattern(&self, value: &str, pattern: &str) -> bool {
562 use regex::Regex;
563
564 if let Ok(re) = Regex::new(pattern) {
566 re.is_match(value)
567 } else {
568 value == pattern
570 }
571 }
572
573 fn evaluate_custom_matcher(&self, expression: &str, request: &ProtocolRequest) -> bool {
575 let expr = expression.trim();
583
584 if expr.contains("==") {
586 self.evaluate_equality(expr, request)
587 } else if expr.contains("=~") {
588 self.evaluate_regex_match(expr, request)
589 } else if expr.contains("contains") {
590 self.evaluate_contains(expr, request)
591 } else {
592 tracing::warn!("Unknown custom matcher expression format: {}", expr);
594 false
595 }
596 }
597
598 fn evaluate_equality(&self, expr: &str, request: &ProtocolRequest) -> bool {
600 let parts: Vec<&str> = expr.split("==").map(|s| s.trim()).collect();
601 if parts.len() != 2 {
602 return false;
603 }
604
605 let field = parts[0];
606 let expected_value = parts[1].trim_matches('"');
607
608 match field {
609 "operation" => request.operation == expected_value,
610 "path" => request.path == expected_value,
611 "topic" => request.topic.as_ref().unwrap_or(&String::new()) == expected_value,
612 "routing_key" => {
613 request.routing_key.as_ref().unwrap_or(&String::new()) == expected_value
614 }
615 _ if field.starts_with("headers.") => {
616 let header_name = &field[8..]; request.metadata.get(header_name).is_some_and(|v| v == expected_value)
618 }
619 _ => {
620 tracing::warn!("Unknown field in equality expression: {}", field);
621 false
622 }
623 }
624 }
625
626 fn evaluate_regex_match(&self, expr: &str, request: &ProtocolRequest) -> bool {
628 let parts: Vec<&str> = expr.split("=~").map(|s| s.trim()).collect();
629 if parts.len() != 2 {
630 return false;
631 }
632
633 let field = parts[0];
634 let pattern = parts[1].trim_matches('"');
635
636 let value: String = match field {
637 "operation" => request.operation.clone(),
638 "path" => request.path.clone(),
639 "topic" => request.topic.clone().unwrap_or_default(),
640 "routing_key" => request.routing_key.clone().unwrap_or_default(),
641 _ if field.starts_with("headers.") => {
642 let header_name = &field[8..]; request.metadata.get(header_name).cloned().unwrap_or_default()
644 }
645 _ => {
646 tracing::warn!("Unknown field in regex expression: {}", field);
647 return false;
648 }
649 };
650
651 use regex::Regex;
652 match Regex::new(pattern) {
653 Ok(re) => re.is_match(&value),
654 Err(e) => {
655 tracing::warn!("Invalid regex pattern '{}': {}", pattern, e);
656 false
657 }
658 }
659 }
660
661 fn evaluate_contains(&self, expr: &str, request: &ProtocolRequest) -> bool {
663 let parts: Vec<&str> = expr.split("contains").map(|s| s.trim()).collect();
664 if parts.len() != 2 {
665 return false;
666 }
667
668 let field = parts[0];
669 let substring = parts[1].trim_matches('"');
670
671 let value: String = match field {
672 "body" => {
673 if let Some(body) = &request.body {
674 String::from_utf8_lossy(body).to_string()
675 } else {
676 return false;
677 }
678 }
679 _ if field.starts_with("headers.") => {
680 let header_name = &field[8..]; request.metadata.get(header_name).cloned().unwrap_or_default()
682 }
683 _ => {
684 tracing::warn!("Unsupported field for contains expression: {}", field);
685 return false;
686 }
687 };
688
689 value.contains(substring)
690 }
691
692 pub fn to_protocol_response(&self) -> Result<ProtocolResponse> {
694 let status = match &self.response.status {
695 FixtureStatus::Http(code) => ResponseStatus::HttpStatus(*code),
696 FixtureStatus::Grpc(code) => ResponseStatus::GrpcStatus(*code),
697 FixtureStatus::Generic(success) => ResponseStatus::GraphQLStatus(*success), FixtureStatus::Custom { code, .. } => ResponseStatus::GrpcStatus(*code), };
700
701 let body = match &self.response.body {
702 Some(serde_json::Value::String(s)) => s.clone().into_bytes(),
703 Some(value) => serde_json::to_string(value)?.into_bytes(),
704 None => Vec::new(),
705 };
706
707 let content_type = self
708 .response
709 .content_type
710 .clone()
711 .unwrap_or_else(|| "application/json".to_string());
712
713 Ok(ProtocolResponse {
714 status,
715 metadata: self.response.headers.clone(),
716 body,
717 content_type,
718 })
719 }
720}
721
722pub struct MiddlewareChain {
724 middleware: Vec<Arc<dyn ProtocolMiddleware>>,
726}
727
728impl MiddlewareChain {
729 pub fn new() -> Self {
731 Self {
732 middleware: Vec::new(),
733 }
734 }
735
736 pub fn with_middleware(mut self, middleware: Arc<dyn ProtocolMiddleware>) -> Self {
738 self.middleware.push(middleware);
739 self
740 }
741
742 pub async fn process_request(
747 &self,
748 request: &mut ProtocolRequest,
749 ) -> Result<Option<ProtocolResponse>> {
750 for middleware in &self.middleware {
751 if middleware.supports_protocol(request.protocol) {
752 match middleware.process_request(request).await? {
753 MiddlewareAction::Continue => {}
754 MiddlewareAction::ShortCircuit(response) => {
755 tracing::debug!(
756 middleware = middleware.name(),
757 "Middleware short-circuited request processing"
758 );
759 return Ok(Some(response));
760 }
761 }
762 }
763 }
764 Ok(None)
765 }
766
767 pub async fn process_response(
769 &self,
770 request: &ProtocolRequest,
771 response: &mut ProtocolResponse,
772 ) -> Result<()> {
773 for middleware in self.middleware.iter().rev() {
774 if middleware.supports_protocol(request.protocol) {
775 middleware.process_response(request, response).await?;
776 }
777 }
778 Ok(())
779 }
780}
781
782impl Default for MiddlewareChain {
783 fn default() -> Self {
784 Self::new()
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn test_protocol_display() {
794 assert_eq!(Protocol::Http.to_string(), "HTTP");
795 assert_eq!(Protocol::GraphQL.to_string(), "GraphQL");
796 assert_eq!(Protocol::Grpc.to_string(), "gRPC");
797 assert_eq!(Protocol::WebSocket.to_string(), "WebSocket");
798 assert_eq!(Protocol::Smtp.to_string(), "SMTP");
799 assert_eq!(Protocol::Mqtt.to_string(), "MQTT");
800 assert_eq!(Protocol::Ftp.to_string(), "FTP");
801 assert_eq!(Protocol::Kafka.to_string(), "Kafka");
802 assert_eq!(Protocol::RabbitMq.to_string(), "RabbitMQ");
803 assert_eq!(Protocol::Amqp.to_string(), "AMQP");
804 assert_eq!(Protocol::Tcp.to_string(), "TCP");
805 }
806
807 #[test]
808 fn test_response_status_is_success() {
809 assert!(ResponseStatus::HttpStatus(200).is_success());
810 assert!(ResponseStatus::HttpStatus(204).is_success());
811 assert!(!ResponseStatus::HttpStatus(404).is_success());
812 assert!(!ResponseStatus::HttpStatus(500).is_success());
813
814 assert!(ResponseStatus::GrpcStatus(0).is_success());
815 assert!(!ResponseStatus::GrpcStatus(2).is_success());
816
817 assert!(ResponseStatus::GraphQLStatus(true).is_success());
818 assert!(!ResponseStatus::GraphQLStatus(false).is_success());
819 }
820
821 #[test]
822 fn test_response_status_as_code() {
823 assert_eq!(ResponseStatus::HttpStatus(200).as_code(), Some(200));
824 assert_eq!(ResponseStatus::GrpcStatus(0).as_code(), Some(0));
825 assert_eq!(ResponseStatus::GraphQLStatus(true).as_code(), None);
826 }
827
828 #[test]
829 fn test_validation_result_success() {
830 let result = ValidationResult::success();
831 assert!(result.valid);
832 assert_eq!(result.errors.len(), 0);
833 assert_eq!(result.warnings.len(), 0);
834 }
835
836 #[test]
837 fn test_validation_result_failure() {
838 let errors = vec![ValidationError {
839 message: "Invalid field".to_string(),
840 path: Some("body.field".to_string()),
841 code: Some("INVALID_FIELD".to_string()),
842 }];
843 let result = ValidationResult::failure(errors);
844 assert!(!result.valid);
845 assert_eq!(result.errors.len(), 1);
846 }
847
848 #[test]
849 fn test_validation_result_with_warning() {
850 let result = ValidationResult::success().with_warning("Deprecated field used".to_string());
851 assert!(result.valid);
852 assert_eq!(result.warnings.len(), 1);
853 }
854
855 #[test]
856 fn test_middleware_chain_creation() {
857 let chain = MiddlewareChain::new();
858 assert_eq!(chain.middleware.len(), 0);
859 }
860
861 #[test]
862 fn test_protocol_request_creation() {
863 let request = ProtocolRequest {
864 protocol: Protocol::Http,
865 operation: "GET".to_string(),
866 path: "/users".to_string(),
867 client_ip: Some("127.0.0.1".to_string()),
868 ..Default::default()
869 };
870 assert_eq!(request.protocol, Protocol::Http);
871 assert_eq!(request.pattern, MessagePattern::RequestResponse);
872 assert_eq!(request.operation, "GET");
873 assert_eq!(request.path, "/users");
874 }
875
876 #[test]
877 fn test_protocol_response_creation() {
878 let response = ProtocolResponse {
879 status: ResponseStatus::HttpStatus(200),
880 metadata: HashMap::new(),
881 body: b"{}".to_vec(),
882 content_type: "application/json".to_string(),
883 };
884 assert!(response.status.is_success());
885 assert_eq!(response.content_type, "application/json");
886 }
887
888 #[test]
889 fn test_unified_fixture_matching() {
890 let fixture = UnifiedFixture {
891 id: "test-fixture".to_string(),
892 name: "Test Fixture".to_string(),
893 description: "A test fixture".to_string(),
894 protocol: Protocol::Http,
895 request: FixtureRequest {
896 pattern: Some(MessagePattern::RequestResponse),
897 operation: Some("GET".to_string()),
898 path: Some("/api/users".to_string()),
899 topic: None,
900 routing_key: None,
901 partition: None,
902 qos: None,
903 headers: HashMap::new(),
904 body_pattern: None,
905 custom_matcher: None,
906 },
907 response: FixtureResponse {
908 status: FixtureStatus::Http(200),
909 headers: HashMap::new(),
910 body: Some(serde_json::json!({"users": ["john", "jane"]})),
911 content_type: Some("application/json".to_string()),
912 delay_ms: 0,
913 template_vars: HashMap::new(),
914 },
915 metadata: HashMap::new(),
916 enabled: true,
917 priority: 0,
918 tags: vec![],
919 };
920
921 let matching_request = ProtocolRequest {
922 protocol: Protocol::Http,
923 pattern: MessagePattern::RequestResponse,
924 operation: "GET".to_string(),
925 path: "/api/users".to_string(),
926 topic: None,
927 routing_key: None,
928 partition: None,
929 qos: None,
930 metadata: HashMap::new(),
931 body: None,
932 client_ip: None,
933 };
934
935 let non_matching_request = ProtocolRequest {
936 protocol: Protocol::Http,
937 pattern: MessagePattern::RequestResponse,
938 operation: "POST".to_string(),
939 path: "/api/users".to_string(),
940 topic: None,
941 routing_key: None,
942 partition: None,
943 qos: None,
944 metadata: HashMap::new(),
945 body: None,
946 client_ip: None,
947 };
948
949 assert!(fixture.matches(&matching_request));
950 assert!(!fixture.matches(&non_matching_request));
951 }
952
953 #[test]
954 fn test_fixture_to_protocol_response() {
955 let fixture = UnifiedFixture {
956 id: "test".to_string(),
957 name: "Test".to_string(),
958 description: "".to_string(),
959 protocol: Protocol::Http,
960 request: FixtureRequest {
961 pattern: None,
962 operation: None,
963 path: None,
964 topic: None,
965 routing_key: None,
966 partition: None,
967 qos: None,
968 headers: HashMap::new(),
969 body_pattern: None,
970 custom_matcher: None,
971 },
972 response: FixtureResponse {
973 status: FixtureStatus::Http(200),
974 headers: {
975 let mut h = HashMap::new();
976 h.insert("content-type".to_string(), "application/json".to_string());
977 h
978 },
979 body: Some(serde_json::json!({"message": "ok"})),
980 content_type: Some("application/json".to_string()),
981 delay_ms: 0,
982 template_vars: HashMap::new(),
983 },
984 metadata: HashMap::new(),
985 enabled: true,
986 priority: 0,
987 tags: vec![],
988 };
989
990 let response = fixture.to_protocol_response().unwrap();
991 assert!(response.status.is_success());
992 assert_eq!(response.content_type, "application/json");
993 assert_eq!(response.metadata.get("content-type"), Some(&"application/json".to_string()));
994 }
995
996 #[test]
997 fn test_fixture_status_serialization() {
998 let status = FixtureStatus::Http(404);
1000 let serialized = serde_json::to_string(&status).unwrap();
1001 assert_eq!(serialized, "404");
1002
1003 let status = FixtureStatus::Grpc(5);
1005 let serialized = serde_json::to_string(&status).unwrap();
1006 assert_eq!(serialized, "5");
1007
1008 let status = FixtureStatus::Generic(true);
1010 let serialized = serde_json::to_string(&status).unwrap();
1011 assert_eq!(serialized, "true");
1012
1013 let status = FixtureStatus::Custom {
1015 code: 500,
1016 message: "Internal Error".to_string(),
1017 };
1018 let serialized = serde_json::to_string(&status).unwrap();
1019 let expected: serde_json::Value =
1020 serde_json::json!({"code": 500, "message": "Internal Error"});
1021 assert_eq!(serde_json::from_str::<serde_json::Value>(&serialized).unwrap(), expected);
1022 }
1023
1024 #[test]
1025 fn test_fixture_pattern_matching() {
1026 let fixture = UnifiedFixture {
1027 id: "test".to_string(),
1028 name: "Test".to_string(),
1029 description: "".to_string(),
1030 protocol: Protocol::Http,
1031 request: FixtureRequest {
1032 pattern: Some(MessagePattern::RequestResponse),
1033 operation: Some("GET".to_string()),
1034 path: Some("/api/.*".to_string()),
1035 topic: None,
1036 routing_key: None,
1037 partition: None,
1038 qos: None,
1039 headers: HashMap::new(),
1040 body_pattern: None,
1041 custom_matcher: None,
1042 },
1043 response: FixtureResponse {
1044 status: FixtureStatus::Http(200),
1045 headers: HashMap::new(),
1046 body: None,
1047 content_type: None,
1048 delay_ms: 0,
1049 template_vars: HashMap::new(),
1050 },
1051 metadata: HashMap::new(),
1052 enabled: true,
1053 priority: 0,
1054 tags: vec![],
1055 };
1056
1057 let request = ProtocolRequest {
1059 protocol: Protocol::Http,
1060 pattern: MessagePattern::RequestResponse,
1061 operation: "GET".to_string(),
1062 path: "/api/users".to_string(),
1063 topic: None,
1064 routing_key: None,
1065 partition: None,
1066 qos: None,
1067 metadata: HashMap::new(),
1068 body: None,
1069 client_ip: None,
1070 };
1071 assert!(fixture.matches(&request));
1072
1073 let grpc_request = ProtocolRequest {
1075 protocol: Protocol::Grpc,
1076 pattern: MessagePattern::RequestResponse,
1077 operation: "GET".to_string(),
1078 path: "/api/users".to_string(),
1079 topic: None,
1080 routing_key: None,
1081 partition: None,
1082 qos: None,
1083 metadata: HashMap::new(),
1084 body: None,
1085 client_ip: None,
1086 };
1087 assert!(!fixture.matches(&grpc_request));
1088
1089 let post_request = ProtocolRequest {
1091 protocol: Protocol::Http,
1092 pattern: MessagePattern::RequestResponse,
1093 operation: "POST".to_string(),
1094 path: "/api/users".to_string(),
1095 topic: None,
1096 routing_key: None,
1097 partition: None,
1098 qos: None,
1099 metadata: HashMap::new(),
1100 body: None,
1101 client_ip: None,
1102 };
1103 assert!(!fixture.matches(&post_request));
1104 }
1105
1106 #[test]
1107 fn test_custom_matcher_equality() {
1108 let fixture = UnifiedFixture {
1109 id: "test".to_string(),
1110 name: "Test".to_string(),
1111 description: "".to_string(),
1112 protocol: Protocol::Http,
1113 request: FixtureRequest {
1114 pattern: Some(MessagePattern::RequestResponse),
1115 operation: Some("GET".to_string()),
1116 path: Some("/api/users".to_string()),
1117 topic: None,
1118 routing_key: None,
1119 partition: None,
1120 qos: None,
1121 headers: HashMap::new(),
1122 body_pattern: None,
1123 custom_matcher: Some("operation == \"GET\"".to_string()),
1124 },
1125 response: FixtureResponse {
1126 status: FixtureStatus::Http(200),
1127 headers: HashMap::new(),
1128 body: None,
1129 content_type: None,
1130 delay_ms: 0,
1131 template_vars: HashMap::new(),
1132 },
1133 metadata: HashMap::new(),
1134 enabled: true,
1135 priority: 0,
1136 tags: vec![],
1137 };
1138
1139 let request = ProtocolRequest {
1141 protocol: Protocol::Http,
1142 pattern: MessagePattern::RequestResponse,
1143 operation: "GET".to_string(),
1144 path: "/api/users".to_string(),
1145 topic: None,
1146 routing_key: None,
1147 partition: None,
1148 qos: None,
1149 metadata: HashMap::new(),
1150 body: None,
1151 client_ip: None,
1152 };
1153 assert!(fixture.matches(&request));
1154
1155 let post_request = ProtocolRequest {
1157 protocol: Protocol::Http,
1158 pattern: MessagePattern::RequestResponse,
1159 operation: "POST".to_string(),
1160 path: "/api/users".to_string(),
1161 topic: None,
1162 routing_key: None,
1163 partition: None,
1164 qos: None,
1165 metadata: HashMap::new(),
1166 body: None,
1167 client_ip: None,
1168 };
1169 assert!(!fixture.matches(&post_request));
1170 }
1171
1172 #[test]
1173 fn test_custom_matcher_regex() {
1174 let fixture = UnifiedFixture {
1175 id: "test".to_string(),
1176 name: "Test".to_string(),
1177 description: "".to_string(),
1178 protocol: Protocol::Http,
1179 request: FixtureRequest {
1180 pattern: Some(MessagePattern::RequestResponse),
1181 operation: Some("GET".to_string()),
1182 path: Some("/api/.*".to_string()),
1183 topic: None,
1184 routing_key: None,
1185 partition: None,
1186 qos: None,
1187 headers: HashMap::new(),
1188 body_pattern: None,
1189 custom_matcher: Some("path =~ \"/api/.*\"".to_string()),
1190 },
1191 response: FixtureResponse {
1192 status: FixtureStatus::Http(200),
1193 headers: HashMap::new(),
1194 body: None,
1195 content_type: None,
1196 delay_ms: 0,
1197 template_vars: HashMap::new(),
1198 },
1199 metadata: HashMap::new(),
1200 enabled: true,
1201 priority: 0,
1202 tags: vec![],
1203 };
1204
1205 let request = ProtocolRequest {
1207 protocol: Protocol::Http,
1208 pattern: MessagePattern::RequestResponse,
1209 operation: "GET".to_string(),
1210 path: "/api/users".to_string(),
1211 topic: None,
1212 routing_key: None,
1213 partition: None,
1214 qos: None,
1215 metadata: HashMap::new(),
1216 body: None,
1217 client_ip: None,
1218 };
1219 assert!(fixture.matches(&request));
1220
1221 let other_request = ProtocolRequest {
1223 protocol: Protocol::Http,
1224 pattern: MessagePattern::RequestResponse,
1225 operation: "GET".to_string(),
1226 path: "/other/path".to_string(),
1227 topic: None,
1228 routing_key: None,
1229 partition: None,
1230 qos: None,
1231 metadata: HashMap::new(),
1232 body: None,
1233 client_ip: None,
1234 };
1235 assert!(!fixture.matches(&other_request));
1236 }
1237
1238 #[test]
1239 fn test_custom_matcher_contains() {
1240 let fixture = UnifiedFixture {
1241 id: "test".to_string(),
1242 name: "Test".to_string(),
1243 description: "".to_string(),
1244 protocol: Protocol::Http,
1245 request: FixtureRequest {
1246 pattern: Some(MessagePattern::RequestResponse),
1247 operation: Some("POST".to_string()),
1248 path: Some("/api/users".to_string()),
1249 topic: None,
1250 routing_key: None,
1251 partition: None,
1252 qos: None,
1253 headers: HashMap::new(),
1254 body_pattern: None,
1255 custom_matcher: Some("body contains \"test\"".to_string()),
1256 },
1257 response: FixtureResponse {
1258 status: FixtureStatus::Http(200),
1259 headers: HashMap::new(),
1260 body: None,
1261 content_type: None,
1262 delay_ms: 0,
1263 template_vars: HashMap::new(),
1264 },
1265 metadata: HashMap::new(),
1266 enabled: true,
1267 priority: 0,
1268 tags: vec![],
1269 };
1270
1271 let request = ProtocolRequest {
1273 protocol: Protocol::Http,
1274 pattern: MessagePattern::RequestResponse,
1275 operation: "POST".to_string(),
1276 path: "/api/users".to_string(),
1277 topic: None,
1278 routing_key: None,
1279 partition: None,
1280 qos: None,
1281 metadata: HashMap::new(),
1282 body: Some(b"{\"name\": \"test user\"}".to_vec()),
1283 client_ip: None,
1284 };
1285 assert!(fixture.matches(&request));
1286
1287 let no_match_request = ProtocolRequest {
1289 protocol: Protocol::Http,
1290 pattern: MessagePattern::RequestResponse,
1291 operation: "POST".to_string(),
1292 path: "/api/users".to_string(),
1293 topic: None,
1294 routing_key: None,
1295 partition: None,
1296 qos: None,
1297 metadata: HashMap::new(),
1298 body: Some(b"{\"name\": \"other user\"}".to_vec()),
1299 client_ip: None,
1300 };
1301 assert!(!fixture.matches(&no_match_request));
1302 }
1303
1304 #[test]
1305 fn test_custom_matcher_header() {
1306 let fixture = UnifiedFixture {
1307 id: "test".to_string(),
1308 name: "Test".to_string(),
1309 description: "".to_string(),
1310 protocol: Protocol::Http,
1311 request: FixtureRequest {
1312 pattern: Some(MessagePattern::RequestResponse),
1313 operation: Some("GET".to_string()),
1314 path: Some("/api/data".to_string()),
1315 topic: None,
1316 routing_key: None,
1317 partition: None,
1318 qos: None,
1319 headers: HashMap::new(),
1320 body_pattern: None,
1321 custom_matcher: Some("headers.content-type == \"application/json\"".to_string()),
1322 },
1323 response: FixtureResponse {
1324 status: FixtureStatus::Http(200),
1325 headers: HashMap::new(),
1326 body: None,
1327 content_type: None,
1328 delay_ms: 0,
1329 template_vars: HashMap::new(),
1330 },
1331 metadata: HashMap::new(),
1332 enabled: true,
1333 priority: 0,
1334 tags: vec![],
1335 };
1336
1337 let mut headers = HashMap::new();
1339 headers.insert("content-type".to_string(), "application/json".to_string());
1340 let request = ProtocolRequest {
1341 protocol: Protocol::Http,
1342 pattern: MessagePattern::RequestResponse,
1343 operation: "GET".to_string(),
1344 path: "/api/data".to_string(),
1345 topic: None,
1346 routing_key: None,
1347 partition: None,
1348 qos: None,
1349 metadata: headers,
1350 body: None,
1351 client_ip: None,
1352 };
1353 assert!(fixture.matches(&request));
1354
1355 let mut wrong_headers = HashMap::new();
1357 wrong_headers.insert("content-type".to_string(), "text/plain".to_string());
1358 let wrong_request = ProtocolRequest {
1359 protocol: Protocol::Http,
1360 pattern: MessagePattern::RequestResponse,
1361 operation: "GET".to_string(),
1362 path: "/api/data".to_string(),
1363 topic: None,
1364 routing_key: None,
1365 partition: None,
1366 qos: None,
1367 metadata: wrong_headers,
1368 body: None,
1369 client_ip: None,
1370 };
1371 assert!(!fixture.matches(&wrong_request));
1372 }
1373}