1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SmtpFixture {
9 pub identifier: String,
11
12 pub name: String,
14
15 #[serde(default)]
17 pub description: String,
18
19 pub match_criteria: MatchCriteria,
21
22 pub response: SmtpResponse,
24
25 #[serde(default)]
27 pub auto_reply: Option<AutoReply>,
28
29 #[serde(default)]
31 pub storage: StorageConfig,
32
33 #[serde(default)]
35 pub behavior: BehaviorConfig,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, Default)]
40pub struct MatchCriteria {
41 #[serde(default)]
43 pub recipient_pattern: Option<String>,
44
45 #[serde(default)]
47 pub sender_pattern: Option<String>,
48
49 #[serde(default)]
51 pub subject_pattern: Option<String>,
52
53 #[serde(default)]
55 pub match_all: bool,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SmtpResponse {
61 pub status_code: u16,
63
64 pub message: String,
66
67 #[serde(default)]
69 pub delay_ms: u64,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AutoReply {
75 pub enabled: bool,
77
78 pub from: String,
80
81 pub to: String,
83
84 pub subject: String,
86
87 pub body: String,
89
90 #[serde(default)]
92 pub html_body: Option<String>,
93
94 #[serde(default)]
96 pub headers: HashMap<String, String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, Default)]
101pub struct StorageConfig {
102 #[serde(default)]
104 pub save_to_mailbox: bool,
105
106 #[serde(default)]
108 pub export_to_file: Option<ExportConfig>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ExportConfig {
114 pub enabled: bool,
116
117 pub path: String,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123pub struct BehaviorConfig {
124 #[serde(default)]
126 pub failure_rate: f64,
127
128 #[serde(default)]
130 pub latency: Option<LatencyConfig>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct LatencyConfig {
136 pub min_ms: u64,
138
139 pub max_ms: u64,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct StoredEmail {
146 pub id: String,
148
149 pub from: String,
151
152 pub to: Vec<String>,
154
155 pub subject: String,
157
158 pub body: String,
160
161 pub headers: HashMap<String, String>,
163
164 pub received_at: chrono::DateTime<chrono::Utc>,
166
167 #[serde(default)]
169 pub raw: Option<Vec<u8>>,
170}
171
172impl SmtpFixture {
173 pub fn matches(&self, from: &str, to: &str, subject: &str) -> bool {
175 use regex::Regex;
176
177 if self.match_criteria.match_all {
179 return true;
180 }
181
182 if let Some(pattern) = &self.match_criteria.recipient_pattern {
184 if let Ok(re) = Regex::new(pattern) {
185 if !re.is_match(to) {
186 return false;
187 }
188 }
189 }
190
191 if let Some(pattern) = &self.match_criteria.sender_pattern {
193 if let Ok(re) = Regex::new(pattern) {
194 if !re.is_match(from) {
195 return false;
196 }
197 }
198 }
199
200 if let Some(pattern) = &self.match_criteria.subject_pattern {
202 if let Ok(re) = Regex::new(pattern) {
203 if !re.is_match(subject) {
204 return false;
205 }
206 }
207 }
208
209 true
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_fixture_matching() {
219 let fixture = SmtpFixture {
220 identifier: "test".to_string(),
221 name: "Test Fixture".to_string(),
222 description: "".to_string(),
223 match_criteria: MatchCriteria {
224 recipient_pattern: Some(r"^user.*@example\.com$".to_string()),
225 sender_pattern: None,
226 subject_pattern: None,
227 match_all: false,
228 },
229 response: SmtpResponse {
230 status_code: 250,
231 message: "OK".to_string(),
232 delay_ms: 0,
233 },
234 auto_reply: None,
235 storage: StorageConfig::default(),
236 behavior: BehaviorConfig::default(),
237 };
238
239 assert!(fixture.matches("sender@test.com", "user123@example.com", "Test"));
240 assert!(!fixture.matches("sender@test.com", "admin@example.com", "Test"));
241 }
242
243 #[test]
244 fn test_match_all_fixture() {
245 let fixture = SmtpFixture {
246 identifier: "default".to_string(),
247 name: "Default Fixture".to_string(),
248 description: "".to_string(),
249 match_criteria: MatchCriteria {
250 recipient_pattern: None,
251 sender_pattern: None,
252 subject_pattern: None,
253 match_all: true,
254 },
255 response: SmtpResponse {
256 status_code: 250,
257 message: "OK".to_string(),
258 delay_ms: 0,
259 },
260 auto_reply: None,
261 storage: StorageConfig::default(),
262 behavior: BehaviorConfig::default(),
263 };
264
265 assert!(fixture.matches("any@sender.com", "any@recipient.com", "Any Subject"));
266 }
267
268 #[test]
269 fn test_fixture_sender_pattern() {
270 let fixture = SmtpFixture {
271 identifier: "test".to_string(),
272 name: "Test Fixture".to_string(),
273 description: "".to_string(),
274 match_criteria: MatchCriteria {
275 recipient_pattern: None,
276 sender_pattern: Some(r"^admin@.*$".to_string()),
277 subject_pattern: None,
278 match_all: false,
279 },
280 response: SmtpResponse {
281 status_code: 250,
282 message: "OK".to_string(),
283 delay_ms: 0,
284 },
285 auto_reply: None,
286 storage: StorageConfig::default(),
287 behavior: BehaviorConfig::default(),
288 };
289
290 assert!(fixture.matches("admin@example.com", "recipient@example.com", "Test"));
291 assert!(!fixture.matches("user@example.com", "recipient@example.com", "Test"));
292 }
293
294 #[test]
295 fn test_fixture_subject_pattern() {
296 let fixture = SmtpFixture {
297 identifier: "test".to_string(),
298 name: "Test Fixture".to_string(),
299 description: "".to_string(),
300 match_criteria: MatchCriteria {
301 recipient_pattern: None,
302 sender_pattern: None,
303 subject_pattern: Some(r"^Important:.*$".to_string()),
304 match_all: false,
305 },
306 response: SmtpResponse {
307 status_code: 250,
308 message: "OK".to_string(),
309 delay_ms: 0,
310 },
311 auto_reply: None,
312 storage: StorageConfig::default(),
313 behavior: BehaviorConfig::default(),
314 };
315
316 assert!(fixture.matches(
317 "sender@example.com",
318 "recipient@example.com",
319 "Important: Action required"
320 ));
321 assert!(!fixture.matches("sender@example.com", "recipient@example.com", "Regular subject"));
322 }
323
324 #[test]
325 fn test_match_criteria_default() {
326 let criteria = MatchCriteria::default();
327 assert!(criteria.recipient_pattern.is_none());
328 assert!(criteria.sender_pattern.is_none());
329 assert!(criteria.subject_pattern.is_none());
330 assert!(!criteria.match_all);
331 }
332
333 #[test]
334 fn test_storage_config_default() {
335 let config = StorageConfig::default();
336 assert!(!config.save_to_mailbox);
337 assert!(config.export_to_file.is_none());
338 }
339
340 #[test]
341 fn test_behavior_config_default() {
342 let config = BehaviorConfig::default();
343 assert_eq!(config.failure_rate, 0.0);
344 assert!(config.latency.is_none());
345 }
346
347 #[test]
348 fn test_stored_email_serialize() {
349 let email = StoredEmail {
350 id: "test-123".to_string(),
351 from: "sender@example.com".to_string(),
352 to: vec!["recipient@example.com".to_string()],
353 subject: "Test Subject".to_string(),
354 body: "Test body content".to_string(),
355 headers: HashMap::from([("Content-Type".to_string(), "text/plain".to_string())]),
356 received_at: chrono::Utc::now(),
357 raw: None,
358 };
359
360 let json = serde_json::to_string(&email).unwrap();
361 assert!(json.contains("test-123"));
362 assert!(json.contains("sender@example.com"));
363 assert!(json.contains("Test Subject"));
364 }
365
366 #[test]
367 fn test_stored_email_deserialize() {
368 let json = r#"{
369 "id": "email-456",
370 "from": "alice@example.com",
371 "to": ["bob@example.com", "carol@example.com"],
372 "subject": "Hello",
373 "body": "Hi there!",
374 "headers": {},
375 "received_at": "2024-01-15T12:00:00Z"
376 }"#;
377 let email: StoredEmail = serde_json::from_str(json).unwrap();
378 assert_eq!(email.id, "email-456");
379 assert_eq!(email.from, "alice@example.com");
380 assert_eq!(email.to.len(), 2);
381 }
382
383 #[test]
384 fn test_smtp_fixture_serialize() {
385 let fixture = SmtpFixture {
386 identifier: "test".to_string(),
387 name: "Test Fixture".to_string(),
388 description: "A test fixture".to_string(),
389 match_criteria: MatchCriteria::default(),
390 response: SmtpResponse {
391 status_code: 250,
392 message: "OK".to_string(),
393 delay_ms: 100,
394 },
395 auto_reply: None,
396 storage: StorageConfig::default(),
397 behavior: BehaviorConfig::default(),
398 };
399
400 let json = serde_json::to_string(&fixture).unwrap();
401 assert!(json.contains("Test Fixture"));
402 assert!(json.contains("250"));
403 }
404
405 #[test]
406 fn test_smtp_response_with_delay() {
407 let response = SmtpResponse {
408 status_code: 550,
409 message: "Mailbox unavailable".to_string(),
410 delay_ms: 500,
411 };
412 assert_eq!(response.status_code, 550);
413 assert_eq!(response.delay_ms, 500);
414 }
415
416 #[test]
417 fn test_auto_reply_config() {
418 let auto_reply = AutoReply {
419 enabled: true,
420 from: "noreply@example.com".to_string(),
421 to: "{{metadata.from}}".to_string(),
422 subject: "Auto Reply".to_string(),
423 body: "Thank you for your email.".to_string(),
424 html_body: Some("<p>Thank you for your email.</p>".to_string()),
425 headers: HashMap::from([("X-Auto-Reply".to_string(), "true".to_string())]),
426 };
427
428 assert!(auto_reply.enabled);
429 assert!(auto_reply.html_body.is_some());
430 }
431
432 #[test]
433 fn test_latency_config() {
434 let latency = LatencyConfig {
435 min_ms: 100,
436 max_ms: 500,
437 };
438 assert_eq!(latency.min_ms, 100);
439 assert_eq!(latency.max_ms, 500);
440 }
441
442 #[test]
443 fn test_export_config() {
444 let export = ExportConfig {
445 enabled: true,
446 path: "/tmp/emails/{{metadata.from}}/{{timestamp}}.eml".to_string(),
447 };
448 assert!(export.enabled);
449 assert!(export.path.contains("{{metadata.from}}"));
450 }
451
452 #[test]
453 fn test_fixture_combined_matching() {
454 let fixture = SmtpFixture {
455 identifier: "combined".to_string(),
456 name: "Combined Match".to_string(),
457 description: "".to_string(),
458 match_criteria: MatchCriteria {
459 recipient_pattern: Some(r".*@example\.com$".to_string()),
460 sender_pattern: Some(r"^admin@.*$".to_string()),
461 subject_pattern: Some(r"^Urgent:.*$".to_string()),
462 match_all: false,
463 },
464 response: SmtpResponse {
465 status_code: 250,
466 message: "OK".to_string(),
467 delay_ms: 0,
468 },
469 auto_reply: None,
470 storage: StorageConfig::default(),
471 behavior: BehaviorConfig::default(),
472 };
473
474 assert!(fixture.matches("admin@test.com", "user@example.com", "Urgent: Review needed"));
476
477 assert!(!fixture.matches("admin@test.com", "user@other.com", "Urgent: Review needed"));
479
480 assert!(!fixture.matches("user@test.com", "user@example.com", "Urgent: Review needed"));
482
483 assert!(!fixture.matches("admin@test.com", "user@example.com", "Regular subject"));
485 }
486
487 #[test]
488 fn test_stored_email_clone() {
489 let email = StoredEmail {
490 id: "test-clone".to_string(),
491 from: "sender@example.com".to_string(),
492 to: vec!["recipient@example.com".to_string()],
493 subject: "Clone Test".to_string(),
494 body: "Test body".to_string(),
495 headers: HashMap::new(),
496 received_at: chrono::Utc::now(),
497 raw: Some(vec![1, 2, 3]),
498 };
499
500 let cloned = email.clone();
501 assert_eq!(email.id, cloned.id);
502 assert_eq!(email.from, cloned.from);
503 assert_eq!(email.raw, cloned.raw);
504 }
505
506 #[test]
507 fn test_fixture_debug() {
508 let fixture = SmtpFixture {
509 identifier: "debug-test".to_string(),
510 name: "Debug Test".to_string(),
511 description: "".to_string(),
512 match_criteria: MatchCriteria::default(),
513 response: SmtpResponse {
514 status_code: 250,
515 message: "OK".to_string(),
516 delay_ms: 0,
517 },
518 auto_reply: None,
519 storage: StorageConfig::default(),
520 behavior: BehaviorConfig::default(),
521 };
522
523 let debug = format!("{:?}", fixture);
524 assert!(debug.contains("SmtpFixture"));
525 assert!(debug.contains("debug-test"));
526 }
527}