1use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8use crate::email::Email;
9use crate::jobs::{Job, JobContext, JobError, JobResult};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SendEmailJob {
35 pub email: Email,
37}
38
39impl SendEmailJob {
40 #[must_use]
42 pub const fn new(email: Email) -> Self {
43 Self { email }
44 }
45}
46
47#[async_trait]
48impl Job for SendEmailJob {
49 type Result = ();
50
51 async fn execute(&self, ctx: &JobContext) -> JobResult<Self::Result> {
52 self.email.validate()
54 .map_err(|e| JobError::ExecutionFailed(format!("Email validation failed: {e}")))?;
55
56 let Some(email_sender) = ctx.email_sender() else {
58 return Err(JobError::ExecutionFailed(
59 "Email sender not available in JobContext".to_string()
60 ));
61 };
62
63 email_sender.send(self.email.clone()).await
65 .map_err(|e| JobError::ExecutionFailed(format!("Email send failed: {e}")))?;
66
67 Ok(())
68 }
69
70 fn max_retries(&self) -> u32 {
71 3
73 }
74
75 fn timeout(&self) -> std::time::Duration {
76 std::time::Duration::from_secs(30)
78 }
79}
80
81#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::email::sender::MockEmailSender; use std::sync::Arc;
89
90 #[test]
91 fn test_send_email_job_creation() {
92 let email = Email::new()
93 .to("user@example.com")
94 .from("noreply@myapp.com")
95 .subject("Test")
96 .text("Hello");
97
98 let job = SendEmailJob::new(email.clone());
99
100 assert_eq!(job.email.to, email.to);
101 assert_eq!(job.email.from, email.from);
102 assert_eq!(job.email.subject, email.subject);
103 }
104
105 #[test]
106 fn test_send_email_job_serialization() {
107 let email = Email::new()
108 .to("user@example.com")
109 .from("noreply@myapp.com")
110 .subject("Test")
111 .text("Hello");
112
113 let job = SendEmailJob::new(email);
114
115 let serialized = serde_json::to_string(&job).unwrap();
117 let deserialized: SendEmailJob = serde_json::from_str(&serialized).unwrap();
118
119 assert_eq!(job.email.to, deserialized.email.to);
120 assert_eq!(job.email.from, deserialized.email.from);
121 }
122
123 #[tokio::test]
124 async fn test_send_email_job_execute_with_sender() {
125 let mut mock_sender = MockEmailSender::new();
126 mock_sender.expect_send().times(1).returning(|_| Ok(()));
127
128 let ctx = JobContext::new().with_email_sender(Arc::new(mock_sender));
129
130 let email = Email::new()
131 .to("user@example.com")
132 .from("noreply@myapp.com")
133 .subject("Test")
134 .text("Hello");
135
136 let job = SendEmailJob::new(email);
137
138 let result = job.execute(&ctx).await;
139 assert!(result.is_ok());
140 }
141
142 #[tokio::test]
143 async fn test_send_email_job_no_sender() {
144 let ctx = JobContext::new(); let email = Email::new()
147 .to("user@example.com")
148 .from("noreply@myapp.com")
149 .subject("Test")
150 .text("Hello");
151
152 let job = SendEmailJob::new(email);
153
154 let result = job.execute(&ctx).await;
155 assert!(result.is_err());
156 assert!(result.unwrap_err().to_string().contains("not available"));
157 }
158
159 #[tokio::test]
160 async fn test_send_email_job_invalid_email() {
161 let mut mock_sender = MockEmailSender::new();
162 mock_sender.expect_send().times(0);
164
165 let ctx = JobContext::new().with_email_sender(Arc::new(mock_sender));
166
167 let email = Email::new()
169 .from("noreply@myapp.com")
170 .subject("Test")
171 .text("Hello");
172
173 let job = SendEmailJob::new(email);
174
175 let result = job.execute(&ctx).await;
176 assert!(result.is_err());
177 assert!(result.unwrap_err().to_string().contains("validation failed"));
178 }
179}