pub mod auth;
pub mod base_registry;
pub mod matcher;
pub mod middleware;
pub mod protocol_registry;
pub mod streaming;
use crate::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
pub use auth::{AuthMiddleware, AuthResult, Claims};
pub use base_registry::{BaseSpecRegistry, ProtocolFixture};
pub use matcher::{FuzzyRequestMatcher, RequestFingerprint, SimpleRequestMatcher};
pub use middleware::{LatencyMiddleware, LoggingMiddleware, MetricsMiddleware};
pub use protocol_registry::{ProtocolHandler, ProtocolRegistry};
pub use streaming::{
MessageBuilder, MessageStream, ProtocolMessage, StreamingMetadata, StreamingProtocol,
StreamingProtocolRegistry,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Protocol {
Http,
GraphQL,
Grpc,
WebSocket,
Smtp,
Mqtt,
Ftp,
Kafka,
RabbitMq,
Amqp,
Tcp,
}
impl fmt::Display for Protocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Protocol::Http => write!(f, "HTTP"),
Protocol::GraphQL => write!(f, "GraphQL"),
Protocol::Grpc => write!(f, "gRPC"),
Protocol::WebSocket => write!(f, "WebSocket"),
Protocol::Smtp => write!(f, "SMTP"),
Protocol::Mqtt => write!(f, "MQTT"),
Protocol::Ftp => write!(f, "FTP"),
Protocol::Kafka => write!(f, "Kafka"),
Protocol::RabbitMq => write!(f, "RabbitMQ"),
Protocol::Amqp => write!(f, "AMQP"),
Protocol::Tcp => write!(f, "TCP"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MessagePattern {
RequestResponse,
OneWay,
PubSub,
Streaming,
}
impl fmt::Display for MessagePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MessagePattern::RequestResponse => write!(f, "Request-Response"),
MessagePattern::OneWay => write!(f, "One-Way"),
MessagePattern::PubSub => write!(f, "Pub-Sub"),
MessagePattern::Streaming => write!(f, "Streaming"),
}
}
}
#[derive(Debug, Clone)]
pub struct ProtocolRequest {
pub protocol: Protocol,
pub pattern: MessagePattern,
pub operation: String,
pub path: String,
pub topic: Option<String>,
pub routing_key: Option<String>,
pub partition: Option<i32>,
pub qos: Option<u8>,
pub metadata: HashMap<String, String>,
pub body: Option<Vec<u8>>,
pub client_ip: Option<String>,
}
impl Default for ProtocolRequest {
fn default() -> Self {
Self {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: String::new(),
path: String::new(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ProtocolResponse {
pub status: ResponseStatus,
pub metadata: HashMap<String, String>,
pub body: Vec<u8>,
pub content_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseStatus {
HttpStatus(u16),
GrpcStatus(i32),
GraphQLStatus(bool),
WebSocketStatus(bool),
SmtpStatus(u16),
MqttStatus(bool),
KafkaStatus(i16),
AmqpStatus(u16),
FtpStatus(u16),
TcpStatus(bool),
}
impl ResponseStatus {
pub fn is_success(&self) -> bool {
match self {
ResponseStatus::HttpStatus(code) => (200..300).contains(code),
ResponseStatus::GrpcStatus(code) => *code == 0, ResponseStatus::GraphQLStatus(success) => *success,
ResponseStatus::WebSocketStatus(success) => *success,
ResponseStatus::SmtpStatus(code) => (200..300).contains(code), ResponseStatus::MqttStatus(success) => *success,
ResponseStatus::KafkaStatus(code) => *code == 0, ResponseStatus::AmqpStatus(code) => (200..300).contains(code), ResponseStatus::FtpStatus(code) => (200..300).contains(code), ResponseStatus::TcpStatus(success) => *success, }
}
pub fn as_code(&self) -> Option<i32> {
match self {
ResponseStatus::HttpStatus(code) => Some(*code as i32),
ResponseStatus::GrpcStatus(code) => Some(*code),
ResponseStatus::SmtpStatus(code) => Some(*code as i32),
ResponseStatus::KafkaStatus(code) => Some(*code as i32),
ResponseStatus::AmqpStatus(code) => Some(*code as i32),
ResponseStatus::FtpStatus(code) => Some(*code as i32),
ResponseStatus::TcpStatus(_) => None, ResponseStatus::GraphQLStatus(_)
| ResponseStatus::WebSocketStatus(_)
| ResponseStatus::MqttStatus(_) => None,
}
}
}
pub trait SpecRegistry: Send + Sync {
fn protocol(&self) -> Protocol;
fn operations(&self) -> Vec<SpecOperation>;
fn find_operation(&self, operation: &str, path: &str) -> Option<SpecOperation>;
fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult>;
fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse>;
}
#[derive(Debug, Clone)]
pub struct SpecOperation {
pub name: String,
pub path: String,
pub operation_type: String,
pub input_schema: Option<String>,
pub output_schema: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub message: String,
pub path: Option<String>,
pub code: Option<String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn failure(errors: Vec<ValidationError>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
}
}
pub fn with_warning(mut self, warning: String) -> Self {
self.warnings.push(warning);
self
}
}
#[derive(Debug)]
pub enum MiddlewareAction {
Continue,
ShortCircuit(ProtocolResponse),
}
#[async_trait::async_trait]
pub trait ProtocolMiddleware: Send + Sync {
fn name(&self) -> &str;
async fn process_request(&self, request: &mut ProtocolRequest) -> Result<MiddlewareAction>;
async fn process_response(
&self,
request: &ProtocolRequest,
response: &mut ProtocolResponse,
) -> Result<()>;
fn supports_protocol(&self, protocol: Protocol) -> bool;
}
pub trait RequestMatcher: Send + Sync {
fn match_score(&self, request: &ProtocolRequest) -> f64;
fn protocol(&self) -> Protocol;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnifiedFixture {
pub id: String,
pub name: String,
#[serde(default)]
pub description: String,
pub protocol: Protocol,
pub request: FixtureRequest,
pub response: FixtureResponse,
#[serde(default)]
pub metadata: HashMap<String, serde_json::Value>,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub priority: i32,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureRequest {
#[serde(default)]
pub pattern: Option<MessagePattern>,
pub operation: Option<String>,
pub path: Option<String>,
pub topic: Option<String>,
pub routing_key: Option<String>,
pub partition: Option<i32>,
pub qos: Option<u8>,
#[serde(default)]
pub headers: HashMap<String, String>,
pub body_pattern: Option<String>,
pub custom_matcher: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureResponse {
pub status: FixtureStatus,
#[serde(default)]
pub headers: HashMap<String, String>,
pub body: Option<serde_json::Value>,
pub content_type: Option<String>,
#[serde(default)]
pub delay_ms: u64,
#[serde(default)]
pub template_vars: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FixtureStatus {
Http(u16),
Grpc(i32),
Generic(bool),
Custom {
code: i32,
message: String,
},
}
fn default_true() -> bool {
true
}
impl UnifiedFixture {
pub fn matches(&self, request: &ProtocolRequest) -> bool {
if request.protocol != self.protocol {
return false;
}
if let Some(pattern) = &self.request.pattern {
if request.pattern != *pattern {
return false;
}
}
if let Some(operation) = &self.request.operation {
if !self.matches_pattern(&request.operation, operation) {
return false;
}
}
if let Some(path) = &self.request.path {
if !self.matches_pattern(&request.path, path) {
return false;
}
}
if let Some(topic) = &self.request.topic {
if !self.matches_pattern(request.topic.as_ref().unwrap_or(&String::new()), topic) {
return false;
}
}
if let Some(routing_key) = &self.request.routing_key {
if !self.matches_pattern(
request.routing_key.as_ref().unwrap_or(&String::new()),
routing_key,
) {
return false;
}
}
if let Some(partition) = self.request.partition {
if request.partition != Some(partition) {
return false;
}
}
if let Some(qos) = self.request.qos {
if request.qos != Some(qos) {
return false;
}
}
for (key, expected_value) in &self.request.headers {
if let Some(actual_value) = request.metadata.get(key) {
if !self.matches_pattern(actual_value, expected_value) {
return false;
}
} else {
return false;
}
}
if let Some(pattern) = &self.request.body_pattern {
if let Some(body) = &request.body {
let body_str = String::from_utf8_lossy(body);
if !self.matches_pattern(&body_str, pattern) {
return false;
}
} else {
return false;
}
}
if let Some(custom_matcher) = &self.request.custom_matcher {
if !self.evaluate_custom_matcher(custom_matcher, request) {
return false;
}
}
true
}
fn matches_pattern(&self, value: &str, pattern: &str) -> bool {
use regex::Regex;
if let Ok(re) = Regex::new(pattern) {
re.is_match(value)
} else {
value == pattern
}
}
fn evaluate_custom_matcher(&self, expression: &str, request: &ProtocolRequest) -> bool {
let expr = expression.trim();
if expr.contains("==") {
self.evaluate_equality(expr, request)
} else if expr.contains("=~") {
self.evaluate_regex_match(expr, request)
} else if expr.contains("contains") {
self.evaluate_contains(expr, request)
} else {
tracing::warn!("Unknown custom matcher expression format: {}", expr);
false
}
}
fn evaluate_equality(&self, expr: &str, request: &ProtocolRequest) -> bool {
let parts: Vec<&str> = expr.split("==").map(|s| s.trim()).collect();
if parts.len() != 2 {
return false;
}
let field = parts[0];
let expected_value = parts[1].trim_matches('"');
match field {
"operation" => request.operation == expected_value,
"path" => request.path == expected_value,
"topic" => request.topic.as_ref().unwrap_or(&String::new()) == expected_value,
"routing_key" => {
request.routing_key.as_ref().unwrap_or(&String::new()) == expected_value
}
_ if field.starts_with("headers.") => {
let header_name = &field[8..]; request.metadata.get(header_name).is_some_and(|v| v == expected_value)
}
_ => {
tracing::warn!("Unknown field in equality expression: {}", field);
false
}
}
}
fn evaluate_regex_match(&self, expr: &str, request: &ProtocolRequest) -> bool {
let parts: Vec<&str> = expr.split("=~").map(|s| s.trim()).collect();
if parts.len() != 2 {
return false;
}
let field = parts[0];
let pattern = parts[1].trim_matches('"');
let value: String = match field {
"operation" => request.operation.clone(),
"path" => request.path.clone(),
"topic" => request.topic.clone().unwrap_or_default(),
"routing_key" => request.routing_key.clone().unwrap_or_default(),
_ if field.starts_with("headers.") => {
let header_name = &field[8..]; request.metadata.get(header_name).cloned().unwrap_or_default()
}
_ => {
tracing::warn!("Unknown field in regex expression: {}", field);
return false;
}
};
use regex::Regex;
match Regex::new(pattern) {
Ok(re) => re.is_match(&value),
Err(e) => {
tracing::warn!("Invalid regex pattern '{}': {}", pattern, e);
false
}
}
}
fn evaluate_contains(&self, expr: &str, request: &ProtocolRequest) -> bool {
let parts: Vec<&str> = expr.split("contains").map(|s| s.trim()).collect();
if parts.len() != 2 {
return false;
}
let field = parts[0];
let substring = parts[1].trim_matches('"');
let value: String = match field {
"body" => {
if let Some(body) = &request.body {
String::from_utf8_lossy(body).to_string()
} else {
return false;
}
}
_ if field.starts_with("headers.") => {
let header_name = &field[8..]; request.metadata.get(header_name).cloned().unwrap_or_default()
}
_ => {
tracing::warn!("Unsupported field for contains expression: {}", field);
return false;
}
};
value.contains(substring)
}
pub fn to_protocol_response(&self) -> Result<ProtocolResponse> {
let status = match &self.response.status {
FixtureStatus::Http(code) => ResponseStatus::HttpStatus(*code),
FixtureStatus::Grpc(code) => ResponseStatus::GrpcStatus(*code),
FixtureStatus::Generic(success) => ResponseStatus::GraphQLStatus(*success), FixtureStatus::Custom { code, .. } => ResponseStatus::GrpcStatus(*code), };
let body = match &self.response.body {
Some(serde_json::Value::String(s)) => s.clone().into_bytes(),
Some(value) => serde_json::to_string(value)?.into_bytes(),
None => Vec::new(),
};
let content_type = self
.response
.content_type
.clone()
.unwrap_or_else(|| "application/json".to_string());
Ok(ProtocolResponse {
status,
metadata: self.response.headers.clone(),
body,
content_type,
})
}
}
pub struct MiddlewareChain {
middleware: Vec<Arc<dyn ProtocolMiddleware>>,
}
impl MiddlewareChain {
pub fn new() -> Self {
Self {
middleware: Vec::new(),
}
}
pub fn with_middleware(mut self, middleware: Arc<dyn ProtocolMiddleware>) -> Self {
self.middleware.push(middleware);
self
}
pub async fn process_request(
&self,
request: &mut ProtocolRequest,
) -> Result<Option<ProtocolResponse>> {
for middleware in &self.middleware {
if middleware.supports_protocol(request.protocol) {
match middleware.process_request(request).await? {
MiddlewareAction::Continue => {}
MiddlewareAction::ShortCircuit(response) => {
tracing::debug!(
middleware = middleware.name(),
"Middleware short-circuited request processing"
);
return Ok(Some(response));
}
}
}
}
Ok(None)
}
pub async fn process_response(
&self,
request: &ProtocolRequest,
response: &mut ProtocolResponse,
) -> Result<()> {
for middleware in self.middleware.iter().rev() {
if middleware.supports_protocol(request.protocol) {
middleware.process_response(request, response).await?;
}
}
Ok(())
}
}
impl Default for MiddlewareChain {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_protocol_display() {
assert_eq!(Protocol::Http.to_string(), "HTTP");
assert_eq!(Protocol::GraphQL.to_string(), "GraphQL");
assert_eq!(Protocol::Grpc.to_string(), "gRPC");
assert_eq!(Protocol::WebSocket.to_string(), "WebSocket");
assert_eq!(Protocol::Smtp.to_string(), "SMTP");
assert_eq!(Protocol::Mqtt.to_string(), "MQTT");
assert_eq!(Protocol::Ftp.to_string(), "FTP");
assert_eq!(Protocol::Kafka.to_string(), "Kafka");
assert_eq!(Protocol::RabbitMq.to_string(), "RabbitMQ");
assert_eq!(Protocol::Amqp.to_string(), "AMQP");
assert_eq!(Protocol::Tcp.to_string(), "TCP");
}
#[test]
fn test_response_status_is_success() {
assert!(ResponseStatus::HttpStatus(200).is_success());
assert!(ResponseStatus::HttpStatus(204).is_success());
assert!(!ResponseStatus::HttpStatus(404).is_success());
assert!(!ResponseStatus::HttpStatus(500).is_success());
assert!(ResponseStatus::GrpcStatus(0).is_success());
assert!(!ResponseStatus::GrpcStatus(2).is_success());
assert!(ResponseStatus::GraphQLStatus(true).is_success());
assert!(!ResponseStatus::GraphQLStatus(false).is_success());
}
#[test]
fn test_response_status_as_code() {
assert_eq!(ResponseStatus::HttpStatus(200).as_code(), Some(200));
assert_eq!(ResponseStatus::GrpcStatus(0).as_code(), Some(0));
assert_eq!(ResponseStatus::GraphQLStatus(true).as_code(), None);
}
#[test]
fn test_validation_result_success() {
let result = ValidationResult::success();
assert!(result.valid);
assert_eq!(result.errors.len(), 0);
assert_eq!(result.warnings.len(), 0);
}
#[test]
fn test_validation_result_failure() {
let errors = vec![ValidationError {
message: "Invalid field".to_string(),
path: Some("body.field".to_string()),
code: Some("INVALID_FIELD".to_string()),
}];
let result = ValidationResult::failure(errors);
assert!(!result.valid);
assert_eq!(result.errors.len(), 1);
}
#[test]
fn test_validation_result_with_warning() {
let result = ValidationResult::success().with_warning("Deprecated field used".to_string());
assert!(result.valid);
assert_eq!(result.warnings.len(), 1);
}
#[test]
fn test_middleware_chain_creation() {
let chain = MiddlewareChain::new();
assert_eq!(chain.middleware.len(), 0);
}
#[test]
fn test_protocol_request_creation() {
let request = ProtocolRequest {
protocol: Protocol::Http,
operation: "GET".to_string(),
path: "/users".to_string(),
client_ip: Some("127.0.0.1".to_string()),
..Default::default()
};
assert_eq!(request.protocol, Protocol::Http);
assert_eq!(request.pattern, MessagePattern::RequestResponse);
assert_eq!(request.operation, "GET");
assert_eq!(request.path, "/users");
}
#[test]
fn test_protocol_response_creation() {
let response = ProtocolResponse {
status: ResponseStatus::HttpStatus(200),
metadata: HashMap::new(),
body: b"{}".to_vec(),
content_type: "application/json".to_string(),
};
assert!(response.status.is_success());
assert_eq!(response.content_type, "application/json");
}
#[test]
fn test_unified_fixture_matching() {
let fixture = UnifiedFixture {
id: "test-fixture".to_string(),
name: "Test Fixture".to_string(),
description: "A test fixture".to_string(),
protocol: Protocol::Http,
request: FixtureRequest {
pattern: Some(MessagePattern::RequestResponse),
operation: Some("GET".to_string()),
path: Some("/api/users".to_string()),
topic: None,
routing_key: None,
partition: None,
qos: None,
headers: HashMap::new(),
body_pattern: None,
custom_matcher: None,
},
response: FixtureResponse {
status: FixtureStatus::Http(200),
headers: HashMap::new(),
body: Some(serde_json::json!({"users": ["john", "jane"]})),
content_type: Some("application/json".to_string()),
delay_ms: 0,
template_vars: HashMap::new(),
},
metadata: HashMap::new(),
enabled: true,
priority: 0,
tags: vec![],
};
let matching_request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
let non_matching_request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "POST".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(fixture.matches(&matching_request));
assert!(!fixture.matches(&non_matching_request));
}
#[test]
fn test_fixture_to_protocol_response() {
let fixture = UnifiedFixture {
id: "test".to_string(),
name: "Test".to_string(),
description: "".to_string(),
protocol: Protocol::Http,
request: FixtureRequest {
pattern: None,
operation: None,
path: None,
topic: None,
routing_key: None,
partition: None,
qos: None,
headers: HashMap::new(),
body_pattern: None,
custom_matcher: None,
},
response: FixtureResponse {
status: FixtureStatus::Http(200),
headers: {
let mut h = HashMap::new();
h.insert("content-type".to_string(), "application/json".to_string());
h
},
body: Some(serde_json::json!({"message": "ok"})),
content_type: Some("application/json".to_string()),
delay_ms: 0,
template_vars: HashMap::new(),
},
metadata: HashMap::new(),
enabled: true,
priority: 0,
tags: vec![],
};
let response = fixture.to_protocol_response().unwrap();
assert!(response.status.is_success());
assert_eq!(response.content_type, "application/json");
assert_eq!(response.metadata.get("content-type"), Some(&"application/json".to_string()));
}
#[test]
fn test_fixture_status_serialization() {
let status = FixtureStatus::Http(404);
let serialized = serde_json::to_string(&status).unwrap();
assert_eq!(serialized, "404");
let status = FixtureStatus::Grpc(5);
let serialized = serde_json::to_string(&status).unwrap();
assert_eq!(serialized, "5");
let status = FixtureStatus::Generic(true);
let serialized = serde_json::to_string(&status).unwrap();
assert_eq!(serialized, "true");
let status = FixtureStatus::Custom {
code: 500,
message: "Internal Error".to_string(),
};
let serialized = serde_json::to_string(&status).unwrap();
let expected: serde_json::Value =
serde_json::json!({"code": 500, "message": "Internal Error"});
assert_eq!(serde_json::from_str::<serde_json::Value>(&serialized).unwrap(), expected);
}
#[test]
fn test_fixture_pattern_matching() {
let fixture = UnifiedFixture {
id: "test".to_string(),
name: "Test".to_string(),
description: "".to_string(),
protocol: Protocol::Http,
request: FixtureRequest {
pattern: Some(MessagePattern::RequestResponse),
operation: Some("GET".to_string()),
path: Some("/api/.*".to_string()),
topic: None,
routing_key: None,
partition: None,
qos: None,
headers: HashMap::new(),
body_pattern: None,
custom_matcher: None,
},
response: FixtureResponse {
status: FixtureStatus::Http(200),
headers: HashMap::new(),
body: None,
content_type: None,
delay_ms: 0,
template_vars: HashMap::new(),
},
metadata: HashMap::new(),
enabled: true,
priority: 0,
tags: vec![],
};
let request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(fixture.matches(&request));
let grpc_request = ProtocolRequest {
protocol: Protocol::Grpc,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(!fixture.matches(&grpc_request));
let post_request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "POST".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(!fixture.matches(&post_request));
}
#[test]
fn test_custom_matcher_equality() {
let fixture = UnifiedFixture {
id: "test".to_string(),
name: "Test".to_string(),
description: "".to_string(),
protocol: Protocol::Http,
request: FixtureRequest {
pattern: Some(MessagePattern::RequestResponse),
operation: Some("GET".to_string()),
path: Some("/api/users".to_string()),
topic: None,
routing_key: None,
partition: None,
qos: None,
headers: HashMap::new(),
body_pattern: None,
custom_matcher: Some("operation == \"GET\"".to_string()),
},
response: FixtureResponse {
status: FixtureStatus::Http(200),
headers: HashMap::new(),
body: None,
content_type: None,
delay_ms: 0,
template_vars: HashMap::new(),
},
metadata: HashMap::new(),
enabled: true,
priority: 0,
tags: vec![],
};
let request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(fixture.matches(&request));
let post_request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "POST".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(!fixture.matches(&post_request));
}
#[test]
fn test_custom_matcher_regex() {
let fixture = UnifiedFixture {
id: "test".to_string(),
name: "Test".to_string(),
description: "".to_string(),
protocol: Protocol::Http,
request: FixtureRequest {
pattern: Some(MessagePattern::RequestResponse),
operation: Some("GET".to_string()),
path: Some("/api/.*".to_string()),
topic: None,
routing_key: None,
partition: None,
qos: None,
headers: HashMap::new(),
body_pattern: None,
custom_matcher: Some("path =~ \"/api/.*\"".to_string()),
},
response: FixtureResponse {
status: FixtureStatus::Http(200),
headers: HashMap::new(),
body: None,
content_type: None,
delay_ms: 0,
template_vars: HashMap::new(),
},
metadata: HashMap::new(),
enabled: true,
priority: 0,
tags: vec![],
};
let request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(fixture.matches(&request));
let other_request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/other/path".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: None,
client_ip: None,
};
assert!(!fixture.matches(&other_request));
}
#[test]
fn test_custom_matcher_contains() {
let fixture = UnifiedFixture {
id: "test".to_string(),
name: "Test".to_string(),
description: "".to_string(),
protocol: Protocol::Http,
request: FixtureRequest {
pattern: Some(MessagePattern::RequestResponse),
operation: Some("POST".to_string()),
path: Some("/api/users".to_string()),
topic: None,
routing_key: None,
partition: None,
qos: None,
headers: HashMap::new(),
body_pattern: None,
custom_matcher: Some("body contains \"test\"".to_string()),
},
response: FixtureResponse {
status: FixtureStatus::Http(200),
headers: HashMap::new(),
body: None,
content_type: None,
delay_ms: 0,
template_vars: HashMap::new(),
},
metadata: HashMap::new(),
enabled: true,
priority: 0,
tags: vec![],
};
let request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "POST".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: Some(b"{\"name\": \"test user\"}".to_vec()),
client_ip: None,
};
assert!(fixture.matches(&request));
let no_match_request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "POST".to_string(),
path: "/api/users".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: HashMap::new(),
body: Some(b"{\"name\": \"other user\"}".to_vec()),
client_ip: None,
};
assert!(!fixture.matches(&no_match_request));
}
#[test]
fn test_custom_matcher_header() {
let fixture = UnifiedFixture {
id: "test".to_string(),
name: "Test".to_string(),
description: "".to_string(),
protocol: Protocol::Http,
request: FixtureRequest {
pattern: Some(MessagePattern::RequestResponse),
operation: Some("GET".to_string()),
path: Some("/api/data".to_string()),
topic: None,
routing_key: None,
partition: None,
qos: None,
headers: HashMap::new(),
body_pattern: None,
custom_matcher: Some("headers.content-type == \"application/json\"".to_string()),
},
response: FixtureResponse {
status: FixtureStatus::Http(200),
headers: HashMap::new(),
body: None,
content_type: None,
delay_ms: 0,
template_vars: HashMap::new(),
},
metadata: HashMap::new(),
enabled: true,
priority: 0,
tags: vec![],
};
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
let request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/api/data".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: headers,
body: None,
client_ip: None,
};
assert!(fixture.matches(&request));
let mut wrong_headers = HashMap::new();
wrong_headers.insert("content-type".to_string(), "text/plain".to_string());
let wrong_request = ProtocolRequest {
protocol: Protocol::Http,
pattern: MessagePattern::RequestResponse,
operation: "GET".to_string(),
path: "/api/data".to_string(),
topic: None,
routing_key: None,
partition: None,
qos: None,
metadata: wrong_headers,
body: None,
client_ip: None,
};
assert!(!fixture.matches(&wrong_request));
}
}