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
30pub use mockforge_foundation::protocol::Protocol;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum MessagePattern {
37 RequestResponse,
39 OneWay,
41 PubSub,
43 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#[derive(Debug, Clone)]
60pub struct ProtocolRequest {
61 pub protocol: Protocol,
63 pub pattern: MessagePattern,
65 pub operation: String,
67 pub path: String,
69 pub topic: Option<String>,
71 pub routing_key: Option<String>,
73 pub partition: Option<i32>,
75 pub qos: Option<u8>,
77 pub metadata: HashMap<String, String>,
79 pub body: Option<Vec<u8>>,
81 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#[derive(Debug, Clone)]
105pub struct ProtocolResponse {
106 pub status: ResponseStatus,
108 pub metadata: HashMap<String, String>,
110 pub body: Vec<u8>,
112 pub content_type: String,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum ResponseStatus {
119 HttpStatus(u16),
121 GrpcStatus(i32),
123 GraphQLStatus(bool),
125 WebSocketStatus(bool),
127 SmtpStatus(u16),
129 MqttStatus(bool),
131 KafkaStatus(i16),
133 AmqpStatus(u16),
135 FtpStatus(u16),
137 TcpStatus(bool),
139}
140
141impl ResponseStatus {
142 pub fn is_success(&self) -> bool {
144 match self {
145 ResponseStatus::HttpStatus(code) => (200..300).contains(code),
146 ResponseStatus::GrpcStatus(code) => *code == 0, ResponseStatus::GraphQLStatus(success) => *success,
148 ResponseStatus::WebSocketStatus(success) => *success,
149 ResponseStatus::SmtpStatus(code) => (200..300).contains(code), ResponseStatus::MqttStatus(success) => *success,
151 ResponseStatus::KafkaStatus(code) => *code == 0, ResponseStatus::AmqpStatus(code) => (200..300).contains(code), ResponseStatus::FtpStatus(code) => (200..300).contains(code), ResponseStatus::TcpStatus(success) => *success, }
156 }
157
158 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, ResponseStatus::GraphQLStatus(_)
169 | ResponseStatus::WebSocketStatus(_)
170 | ResponseStatus::MqttStatus(_) => None,
171 }
172 }
173}
174
175pub trait SpecRegistry: Send + Sync {
177 fn protocol(&self) -> Protocol;
179
180 fn operations(&self) -> Vec<SpecOperation>;
182
183 fn find_operation(&self, operation: &str, path: &str) -> Option<SpecOperation>;
185
186 fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult>;
188
189 fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse>;
191}
192
193#[derive(Debug, Clone)]
195pub struct SpecOperation {
196 pub name: String,
198 pub path: String,
200 pub operation_type: String,
202 pub input_schema: Option<String>,
204 pub output_schema: Option<String>,
206 pub metadata: HashMap<String, String>,
208}
209
210#[derive(Debug, Clone)]
212pub struct ValidationResult {
213 pub valid: bool,
215 pub errors: Vec<ValidationError>,
217 pub warnings: Vec<String>,
219}
220
221#[derive(Debug, Clone)]
223pub struct ValidationError {
224 pub message: String,
226 pub path: Option<String>,
228 pub code: Option<String>,
230}
231
232impl ValidationResult {
233 pub fn success() -> Self {
235 Self {
236 valid: true,
237 errors: Vec::new(),
238 warnings: Vec::new(),
239 }
240 }
241
242 pub fn failure(errors: Vec<ValidationError>) -> Self {
244 Self {
245 valid: false,
246 errors,
247 warnings: Vec::new(),
248 }
249 }
250
251 pub fn with_warning(mut self, warning: String) -> Self {
253 self.warnings.push(warning);
254 self
255 }
256}
257
258#[derive(Debug)]
263pub enum MiddlewareAction {
264 Continue,
266 ShortCircuit(ProtocolResponse),
268}
269
270#[async_trait::async_trait]
272pub trait ProtocolMiddleware: Send + Sync {
273 fn name(&self) -> &str;
275
276 async fn process_request(&self, request: &mut ProtocolRequest) -> Result<MiddlewareAction>;
281
282 async fn process_response(
284 &self,
285 request: &ProtocolRequest,
286 response: &mut ProtocolResponse,
287 ) -> Result<()>;
288
289 fn supports_protocol(&self, protocol: Protocol) -> bool;
291}
292
293pub trait RequestMatcher: Send + Sync {
295 fn match_score(&self, request: &ProtocolRequest) -> f64;
297
298 fn protocol(&self) -> Protocol;
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct UnifiedFixture {
305 pub id: String,
307
308 pub name: String,
310
311 #[serde(default)]
313 pub description: String,
314
315 pub protocol: Protocol,
317
318 pub request: FixtureRequest,
320
321 pub response: FixtureResponse,
323
324 #[serde(default)]
326 pub metadata: HashMap<String, serde_json::Value>,
327
328 #[serde(default = "default_true")]
330 pub enabled: bool,
331
332 #[serde(default)]
334 pub priority: i32,
335
336 #[serde(default)]
338 pub tags: Vec<String>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct FixtureRequest {
344 #[serde(default)]
346 pub pattern: Option<MessagePattern>,
347
348 pub operation: Option<String>,
350
351 pub path: Option<String>,
353
354 pub topic: Option<String>,
356
357 pub routing_key: Option<String>,
359
360 pub partition: Option<i32>,
362
363 pub qos: Option<u8>,
365
366 #[serde(default)]
368 pub headers: HashMap<String, String>,
369
370 pub body_pattern: Option<String>,
372
373 pub custom_matcher: Option<String>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct FixtureResponse {
380 pub status: FixtureStatus,
382
383 #[serde(default)]
385 pub headers: HashMap<String, String>,
386
387 pub body: Option<serde_json::Value>,
389
390 pub content_type: Option<String>,
392
393 #[serde(default)]
395 pub delay_ms: u64,
396
397 #[serde(default)]
399 pub template_vars: HashMap<String, serde_json::Value>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404#[serde(untagged)]
405pub enum FixtureStatus {
406 Http(u16),
408 Grpc(i32),
410 Generic(bool),
412 Custom {
414 code: i32,
416 message: String,
418 },
419}
420
421fn default_true() -> bool {
422 true
423}
424
425impl UnifiedFixture {
426 pub fn matches(&self, request: &ProtocolRequest) -> bool {
428 if request.protocol != self.protocol {
430 return false;
431 }
432
433 if let Some(pattern) = &self.request.pattern {
435 if request.pattern != *pattern {
436 return false;
437 }
438 }
439
440 if let Some(operation) = &self.request.operation {
442 if !self.matches_pattern(&request.operation, operation) {
443 return false;
444 }
445 }
446
447 if let Some(path) = &self.request.path {
449 if !self.matches_pattern(&request.path, path) {
450 return false;
451 }
452 }
453
454 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 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 if let Some(partition) = self.request.partition {
473 if request.partition != Some(partition) {
474 return false;
475 }
476 }
477
478 if let Some(qos) = self.request.qos {
480 if request.qos != Some(qos) {
481 return false;
482 }
483 }
484
485 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 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 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 fn matches_pattern(&self, value: &str, pattern: &str) -> bool {
520 use regex::Regex;
521
522 if let Ok(re) = Regex::new(pattern) {
524 re.is_match(value)
525 } else {
526 value == pattern
528 }
529 }
530
531 fn evaluate_custom_matcher(&self, expression: &str, request: &ProtocolRequest) -> bool {
533 let expr = expression.trim();
541
542 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 tracing::warn!("Unknown custom matcher expression format: {}", expr);
552 false
553 }
554 }
555
556 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..]; 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 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..]; 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 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..]; 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 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), FixtureStatus::Custom { code, .. } => ResponseStatus::GrpcStatus(*code), };
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
680pub struct MiddlewareChain {
682 middleware: Vec<Arc<dyn ProtocolMiddleware>>,
684}
685
686impl MiddlewareChain {
687 pub fn new() -> Self {
689 Self {
690 middleware: Vec::new(),
691 }
692 }
693
694 pub fn with_middleware(mut self, middleware: Arc<dyn ProtocolMiddleware>) -> Self {
696 self.middleware.push(middleware);
697 self
698 }
699
700 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 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 let status = FixtureStatus::Http(404);
958 let serialized = serde_json::to_string(&status).unwrap();
959 assert_eq!(serialized, "404");
960
961 let status = FixtureStatus::Grpc(5);
963 let serialized = serde_json::to_string(&status).unwrap();
964 assert_eq!(serialized, "5");
965
966 let status = FixtureStatus::Generic(true);
968 let serialized = serde_json::to_string(&status).unwrap();
969 assert_eq!(serialized, "true");
970
971 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 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 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 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 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 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 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 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 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 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 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 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}