use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmtpFixture {
pub identifier: String,
pub name: String,
#[serde(default)]
pub description: String,
pub match_criteria: MatchCriteria,
pub response: SmtpResponse,
#[serde(default)]
pub auto_reply: Option<AutoReply>,
#[serde(default)]
pub storage: StorageConfig,
#[serde(default)]
pub behavior: BehaviorConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MatchCriteria {
#[serde(default)]
pub recipient_pattern: Option<String>,
#[serde(default)]
pub sender_pattern: Option<String>,
#[serde(default)]
pub subject_pattern: Option<String>,
#[serde(default)]
pub match_all: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmtpResponse {
pub status_code: u16,
pub message: String,
#[serde(default)]
pub delay_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoReply {
pub enabled: bool,
pub from: String,
pub to: String,
pub subject: String,
pub body: String,
#[serde(default)]
pub html_body: Option<String>,
#[serde(default)]
pub headers: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StorageConfig {
#[serde(default)]
pub save_to_mailbox: bool,
#[serde(default)]
pub export_to_file: Option<ExportConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportConfig {
pub enabled: bool,
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BehaviorConfig {
#[serde(default)]
pub failure_rate: f64,
#[serde(default)]
pub latency: Option<LatencyConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatencyConfig {
pub min_ms: u64,
pub max_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredEmail {
pub id: String,
pub from: String,
pub to: Vec<String>,
pub subject: String,
pub body: String,
pub headers: HashMap<String, String>,
pub received_at: chrono::DateTime<chrono::Utc>,
#[serde(default)]
pub raw: Option<Vec<u8>>,
}
impl SmtpFixture {
pub fn matches(&self, from: &str, to: &str, subject: &str) -> bool {
use regex::Regex;
if self.match_criteria.match_all {
return true;
}
if let Some(pattern) = &self.match_criteria.recipient_pattern {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(to) {
return false;
}
}
}
if let Some(pattern) = &self.match_criteria.sender_pattern {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(from) {
return false;
}
}
}
if let Some(pattern) = &self.match_criteria.subject_pattern {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(subject) {
return false;
}
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixture_matching() {
let fixture = SmtpFixture {
identifier: "test".to_string(),
name: "Test Fixture".to_string(),
description: "".to_string(),
match_criteria: MatchCriteria {
recipient_pattern: Some(r"^user.*@example\.com$".to_string()),
sender_pattern: None,
subject_pattern: None,
match_all: false,
},
response: SmtpResponse {
status_code: 250,
message: "OK".to_string(),
delay_ms: 0,
},
auto_reply: None,
storage: StorageConfig::default(),
behavior: BehaviorConfig::default(),
};
assert!(fixture.matches("sender@test.com", "user123@example.com", "Test"));
assert!(!fixture.matches("sender@test.com", "admin@example.com", "Test"));
}
#[test]
fn test_match_all_fixture() {
let fixture = SmtpFixture {
identifier: "default".to_string(),
name: "Default Fixture".to_string(),
description: "".to_string(),
match_criteria: MatchCriteria {
recipient_pattern: None,
sender_pattern: None,
subject_pattern: None,
match_all: true,
},
response: SmtpResponse {
status_code: 250,
message: "OK".to_string(),
delay_ms: 0,
},
auto_reply: None,
storage: StorageConfig::default(),
behavior: BehaviorConfig::default(),
};
assert!(fixture.matches("any@sender.com", "any@recipient.com", "Any Subject"));
}
#[test]
fn test_fixture_sender_pattern() {
let fixture = SmtpFixture {
identifier: "test".to_string(),
name: "Test Fixture".to_string(),
description: "".to_string(),
match_criteria: MatchCriteria {
recipient_pattern: None,
sender_pattern: Some(r"^admin@.*$".to_string()),
subject_pattern: None,
match_all: false,
},
response: SmtpResponse {
status_code: 250,
message: "OK".to_string(),
delay_ms: 0,
},
auto_reply: None,
storage: StorageConfig::default(),
behavior: BehaviorConfig::default(),
};
assert!(fixture.matches("admin@example.com", "recipient@example.com", "Test"));
assert!(!fixture.matches("user@example.com", "recipient@example.com", "Test"));
}
#[test]
fn test_fixture_subject_pattern() {
let fixture = SmtpFixture {
identifier: "test".to_string(),
name: "Test Fixture".to_string(),
description: "".to_string(),
match_criteria: MatchCriteria {
recipient_pattern: None,
sender_pattern: None,
subject_pattern: Some(r"^Important:.*$".to_string()),
match_all: false,
},
response: SmtpResponse {
status_code: 250,
message: "OK".to_string(),
delay_ms: 0,
},
auto_reply: None,
storage: StorageConfig::default(),
behavior: BehaviorConfig::default(),
};
assert!(fixture.matches(
"sender@example.com",
"recipient@example.com",
"Important: Action required"
));
assert!(!fixture.matches("sender@example.com", "recipient@example.com", "Regular subject"));
}
#[test]
fn test_match_criteria_default() {
let criteria = MatchCriteria::default();
assert!(criteria.recipient_pattern.is_none());
assert!(criteria.sender_pattern.is_none());
assert!(criteria.subject_pattern.is_none());
assert!(!criteria.match_all);
}
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert!(!config.save_to_mailbox);
assert!(config.export_to_file.is_none());
}
#[test]
fn test_behavior_config_default() {
let config = BehaviorConfig::default();
assert_eq!(config.failure_rate, 0.0);
assert!(config.latency.is_none());
}
#[test]
fn test_stored_email_serialize() {
let email = StoredEmail {
id: "test-123".to_string(),
from: "sender@example.com".to_string(),
to: vec!["recipient@example.com".to_string()],
subject: "Test Subject".to_string(),
body: "Test body content".to_string(),
headers: HashMap::from([("Content-Type".to_string(), "text/plain".to_string())]),
received_at: chrono::Utc::now(),
raw: None,
};
let json = serde_json::to_string(&email).unwrap();
assert!(json.contains("test-123"));
assert!(json.contains("sender@example.com"));
assert!(json.contains("Test Subject"));
}
#[test]
fn test_stored_email_deserialize() {
let json = r#"{
"id": "email-456",
"from": "alice@example.com",
"to": ["bob@example.com", "carol@example.com"],
"subject": "Hello",
"body": "Hi there!",
"headers": {},
"received_at": "2024-01-15T12:00:00Z"
}"#;
let email: StoredEmail = serde_json::from_str(json).unwrap();
assert_eq!(email.id, "email-456");
assert_eq!(email.from, "alice@example.com");
assert_eq!(email.to.len(), 2);
}
#[test]
fn test_smtp_fixture_serialize() {
let fixture = SmtpFixture {
identifier: "test".to_string(),
name: "Test Fixture".to_string(),
description: "A test fixture".to_string(),
match_criteria: MatchCriteria::default(),
response: SmtpResponse {
status_code: 250,
message: "OK".to_string(),
delay_ms: 100,
},
auto_reply: None,
storage: StorageConfig::default(),
behavior: BehaviorConfig::default(),
};
let json = serde_json::to_string(&fixture).unwrap();
assert!(json.contains("Test Fixture"));
assert!(json.contains("250"));
}
#[test]
fn test_smtp_response_with_delay() {
let response = SmtpResponse {
status_code: 550,
message: "Mailbox unavailable".to_string(),
delay_ms: 500,
};
assert_eq!(response.status_code, 550);
assert_eq!(response.delay_ms, 500);
}
#[test]
fn test_auto_reply_config() {
let auto_reply = AutoReply {
enabled: true,
from: "noreply@example.com".to_string(),
to: "{{metadata.from}}".to_string(),
subject: "Auto Reply".to_string(),
body: "Thank you for your email.".to_string(),
html_body: Some("<p>Thank you for your email.</p>".to_string()),
headers: HashMap::from([("X-Auto-Reply".to_string(), "true".to_string())]),
};
assert!(auto_reply.enabled);
assert!(auto_reply.html_body.is_some());
}
#[test]
fn test_latency_config() {
let latency = LatencyConfig {
min_ms: 100,
max_ms: 500,
};
assert_eq!(latency.min_ms, 100);
assert_eq!(latency.max_ms, 500);
}
#[test]
fn test_export_config() {
let export = ExportConfig {
enabled: true,
path: "/tmp/emails/{{metadata.from}}/{{timestamp}}.eml".to_string(),
};
assert!(export.enabled);
assert!(export.path.contains("{{metadata.from}}"));
}
#[test]
fn test_fixture_combined_matching() {
let fixture = SmtpFixture {
identifier: "combined".to_string(),
name: "Combined Match".to_string(),
description: "".to_string(),
match_criteria: MatchCriteria {
recipient_pattern: Some(r".*@example\.com$".to_string()),
sender_pattern: Some(r"^admin@.*$".to_string()),
subject_pattern: Some(r"^Urgent:.*$".to_string()),
match_all: false,
},
response: SmtpResponse {
status_code: 250,
message: "OK".to_string(),
delay_ms: 0,
},
auto_reply: None,
storage: StorageConfig::default(),
behavior: BehaviorConfig::default(),
};
assert!(fixture.matches("admin@test.com", "user@example.com", "Urgent: Review needed"));
assert!(!fixture.matches("admin@test.com", "user@other.com", "Urgent: Review needed"));
assert!(!fixture.matches("user@test.com", "user@example.com", "Urgent: Review needed"));
assert!(!fixture.matches("admin@test.com", "user@example.com", "Regular subject"));
}
#[test]
fn test_stored_email_clone() {
let email = StoredEmail {
id: "test-clone".to_string(),
from: "sender@example.com".to_string(),
to: vec!["recipient@example.com".to_string()],
subject: "Clone Test".to_string(),
body: "Test body".to_string(),
headers: HashMap::new(),
received_at: chrono::Utc::now(),
raw: Some(vec![1, 2, 3]),
};
let cloned = email.clone();
assert_eq!(email.id, cloned.id);
assert_eq!(email.from, cloned.from);
assert_eq!(email.raw, cloned.raw);
}
#[test]
fn test_fixture_debug() {
let fixture = SmtpFixture {
identifier: "debug-test".to_string(),
name: "Debug Test".to_string(),
description: "".to_string(),
match_criteria: MatchCriteria::default(),
response: SmtpResponse {
status_code: 250,
message: "OK".to_string(),
delay_ms: 0,
},
auto_reply: None,
storage: StorageConfig::default(),
behavior: BehaviorConfig::default(),
};
let debug = format!("{:?}", fixture);
assert!(debug.contains("SmtpFixture"));
assert!(debug.contains("debug-test"));
}
}