1use crate::{
6 models::{
7 NotificationMessage, ScriptLanguage, SecretString, SecretValue, Trigger, TriggerType,
8 TriggerTypeConfig, WebhookPayloadMode,
9 },
10 utils::RetryConfig,
11};
12use email_address::EmailAddress;
13
14pub struct TriggerBuilder {
16 name: String,
17 trigger_type: TriggerType,
18 config: TriggerTypeConfig,
19}
20
21impl Default for TriggerBuilder {
22 fn default() -> Self {
23 Self {
24 name: "test_trigger".to_string(),
25 trigger_type: TriggerType::Webhook,
26 config: TriggerTypeConfig::Webhook {
27 url: SecretValue::Plain(SecretString::new(
28 "https://api.example.com/webhook".to_string(),
29 )),
30 secret: None,
31 method: Some("POST".to_string()),
32 headers: None,
33 message: NotificationMessage {
34 title: "Alert".to_string(),
35 body: "Test message".to_string(),
36 },
37 payload_mode: WebhookPayloadMode::default(),
38 retry_policy: RetryConfig::default(),
39 },
40 }
41 }
42}
43
44impl TriggerBuilder {
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn name(mut self, name: &str) -> Self {
50 self.name = name.to_string();
51 self
52 }
53
54 pub fn config(mut self, config: TriggerTypeConfig) -> Self {
55 self.config = config;
56 self
57 }
58
59 pub fn webhook(mut self, url: &str) -> Self {
60 self.trigger_type = TriggerType::Webhook;
61 self.config = TriggerTypeConfig::Webhook {
62 url: SecretValue::Plain(SecretString::new(url.to_string())),
63 secret: None,
64 method: Some("POST".to_string()),
65 headers: None,
66 message: NotificationMessage {
67 title: "Alert".to_string(),
68 body: "Test message".to_string(),
69 },
70 payload_mode: WebhookPayloadMode::default(),
71 retry_policy: RetryConfig::default(),
72 };
73 self
74 }
75
76 pub fn slack(mut self, webhook_url: &str) -> Self {
77 self.trigger_type = TriggerType::Slack;
78 self.config = TriggerTypeConfig::Slack {
79 slack_url: SecretValue::Plain(SecretString::new(webhook_url.to_string())),
80 message: NotificationMessage {
81 title: "Alert".to_string(),
82 body: "Test message".to_string(),
83 },
84 retry_policy: RetryConfig::default(),
85 };
86 self
87 }
88
89 pub fn discord(mut self, webhook_url: &str) -> Self {
90 self.trigger_type = TriggerType::Discord;
91 self.config = TriggerTypeConfig::Discord {
92 discord_url: SecretValue::Plain(SecretString::new(webhook_url.to_string())),
93 message: NotificationMessage {
94 title: "Alert".to_string(),
95 body: "Test message".to_string(),
96 },
97 retry_policy: RetryConfig::default(),
98 };
99 self
100 }
101
102 pub fn telegram(mut self, token: &str, chat_id: &str, disable_web_preview: bool) -> Self {
103 self.trigger_type = TriggerType::Telegram;
104 self.config = TriggerTypeConfig::Telegram {
105 token: SecretValue::Plain(SecretString::new(token.to_string())),
106 chat_id: chat_id.to_string(),
107 disable_web_preview: Some(disable_web_preview),
108 message: NotificationMessage {
109 title: "Test title".to_string(),
110 body: "Test message".to_string(),
111 },
112 retry_policy: RetryConfig::default(),
113 };
114 self
115 }
116
117 pub fn telegram_token(mut self, token: SecretValue) -> Self {
118 if let TriggerTypeConfig::Telegram { token: t, .. } = &mut self.config {
119 *t = token;
120 }
121 self
122 }
123
124 pub fn script(mut self, script_path: &str, language: ScriptLanguage) -> Self {
125 self.trigger_type = TriggerType::Script;
126 self.config = TriggerTypeConfig::Script {
127 script_path: script_path.to_string(),
128 arguments: None,
129 language,
130 timeout_ms: 1000,
131 };
132 self
133 }
134
135 pub fn script_arguments(mut self, arguments: Vec<String>) -> Self {
136 if let TriggerTypeConfig::Script { arguments: a, .. } = &mut self.config {
137 *a = Some(arguments);
138 }
139 self
140 }
141
142 pub fn script_timeout_ms(mut self, timeout_ms: u32) -> Self {
143 if let TriggerTypeConfig::Script { timeout_ms: t, .. } = &mut self.config {
144 *t = timeout_ms;
145 }
146 self
147 }
148
149 pub fn message(mut self, title: &str, body: &str) -> Self {
150 match &mut self.config {
151 TriggerTypeConfig::Webhook { message, .. }
152 | TriggerTypeConfig::Slack { message, .. }
153 | TriggerTypeConfig::Discord { message, .. }
154 | TriggerTypeConfig::Telegram { message, .. }
155 | TriggerTypeConfig::Email { message, .. } => {
156 message.title = title.to_string();
157 message.body = body.to_string();
158 }
159 _ => {}
160 }
161 self
162 }
163
164 pub fn trigger_type(mut self, trigger_type: TriggerType) -> Self {
165 self.trigger_type = trigger_type;
166 self
167 }
168
169 pub fn email(
170 mut self,
171 host: &str,
172 username: &str,
173 password: &str,
174 sender: &str,
175 recipients: Vec<&str>,
176 ) -> Self {
177 self.trigger_type = TriggerType::Email;
178 self.config = TriggerTypeConfig::Email {
179 host: host.to_string(),
180 port: Some(587),
181 username: SecretValue::Plain(SecretString::new(username.to_string())),
182 password: SecretValue::Plain(SecretString::new(password.to_string())),
183 message: NotificationMessage {
184 title: "Test Subject".to_string(),
185 body: "Test Body".to_string(),
186 },
187 sender: EmailAddress::new_unchecked(sender),
188 recipients: recipients
189 .into_iter()
190 .map(EmailAddress::new_unchecked)
191 .collect(),
192 retry_policy: RetryConfig::default(),
193 };
194 self
195 }
196
197 pub fn email_port(mut self, port: u16) -> Self {
198 if let TriggerTypeConfig::Email { port: p, .. } = &mut self.config {
199 *p = Some(port);
200 }
201 self
202 }
203
204 pub fn email_subject(mut self, subject: &str) -> Self {
205 if let TriggerTypeConfig::Email { message, .. } = &mut self.config {
206 message.title = subject.to_string();
207 }
208 self
209 }
210
211 pub fn email_username(mut self, username: SecretValue) -> Self {
212 if let TriggerTypeConfig::Email { username: u, .. } = &mut self.config {
213 *u = username;
214 }
215 self
216 }
217
218 pub fn email_password(mut self, password: SecretValue) -> Self {
219 if let TriggerTypeConfig::Email { password: p, .. } = &mut self.config {
220 *p = password;
221 }
222 self
223 }
224
225 pub fn webhook_method(mut self, method: &str) -> Self {
226 if let TriggerTypeConfig::Webhook { method: m, .. } = &mut self.config {
227 *m = Some(method.to_string());
228 }
229 self
230 }
231
232 pub fn webhook_secret(mut self, secret: SecretValue) -> Self {
233 if let TriggerTypeConfig::Webhook { secret: s, .. } = &mut self.config {
234 *s = Some(secret);
235 }
236 self
237 }
238
239 pub fn webhook_headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
240 if let TriggerTypeConfig::Webhook { headers: h, .. } = &mut self.config {
241 *h = Some(headers);
242 }
243 self
244 }
245
246 pub fn webhook_payload_mode(mut self, mode: WebhookPayloadMode) -> Self {
247 if let TriggerTypeConfig::Webhook { payload_mode, .. } = &mut self.config {
248 *payload_mode = mode;
249 }
250 self
251 }
252
253 pub fn url(mut self, url: SecretValue) -> Self {
254 self.config = match self.config {
255 TriggerTypeConfig::Webhook {
256 url: _,
257 method,
258 headers,
259 secret,
260 message,
261 payload_mode,
262 retry_policy,
263 } => TriggerTypeConfig::Webhook {
264 url,
265 method,
266 headers,
267 secret,
268 message,
269 payload_mode,
270 retry_policy,
271 },
272 TriggerTypeConfig::Discord {
273 discord_url: _,
274 message,
275 retry_policy,
276 } => TriggerTypeConfig::Discord {
277 discord_url: url,
278 message,
279 retry_policy,
280 },
281 TriggerTypeConfig::Slack {
282 slack_url: _,
283 message,
284 retry_policy,
285 } => TriggerTypeConfig::Slack {
286 slack_url: url,
287 message,
288 retry_policy,
289 },
290 config => config,
291 };
292 self
293 }
294
295 pub fn build(self) -> Trigger {
296 Trigger {
297 name: self.name,
298 trigger_type: self.trigger_type,
299 config: self.config,
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_default_trigger() {
310 let trigger = TriggerBuilder::new().build();
311
312 assert_eq!(trigger.name, "test_trigger");
313 assert_eq!(trigger.trigger_type, TriggerType::Webhook);
314
315 match trigger.config {
316 TriggerTypeConfig::Webhook { url, method, .. } => {
317 assert_eq!(url.as_ref().to_string(), "https://api.example.com/webhook");
318 assert_eq!(method, Some("POST".to_string()));
319 }
320 _ => panic!("Expected webhook config"),
321 }
322 }
323
324 #[test]
325 fn test_trigger_with_config() {
326 let trigger = TriggerBuilder::new()
327 .name("my_trigger")
328 .config(TriggerTypeConfig::Webhook {
329 url: SecretValue::Plain(SecretString::new(
330 "https://api.example.com/webhook".to_string(),
331 )),
332 secret: Some(SecretValue::Plain(SecretString::new("secret".to_string()))),
333 method: Some("POST".to_string()),
334 headers: None,
335 message: NotificationMessage {
336 title: "Alert".to_string(),
337 body: "Test message".to_string(),
338 },
339 payload_mode: WebhookPayloadMode::default(),
340 retry_policy: RetryConfig::default(),
341 })
342 .build();
343
344 assert_eq!(trigger.name, "my_trigger");
345 assert_eq!(trigger.trigger_type, TriggerType::Webhook);
346
347 match trigger.config {
348 TriggerTypeConfig::Webhook { url, method, .. } => {
349 assert_eq!(url.as_ref().to_string(), "https://api.example.com/webhook");
350 assert_eq!(method, Some("POST".to_string()));
351 }
352 _ => panic!("Expected webhook config"),
353 }
354 }
355
356 #[test]
357 fn test_webhook_trigger() {
358 let trigger = TriggerBuilder::new()
359 .name("my_webhook")
360 .webhook("https://webhook.example.com")
361 .message("Custom Alert", "Something happened!")
362 .build();
363
364 assert_eq!(trigger.name, "my_webhook");
365 assert_eq!(trigger.trigger_type, TriggerType::Webhook);
366
367 match trigger.config {
368 TriggerTypeConfig::Webhook { url, message, .. } => {
369 assert_eq!(url.as_ref().to_string(), "https://webhook.example.com");
370 assert_eq!(message.title, "Custom Alert");
371 assert_eq!(message.body, "Something happened!");
372 }
373 _ => panic!("Expected webhook config"),
374 }
375 }
376
377 #[test]
378 fn test_webhook_trigger_with_config() {
379 let mut headers = std::collections::HashMap::new();
380 headers.insert("Content-Type".to_string(), "application/json".to_string());
381
382 let trigger = TriggerBuilder::new()
383 .name("my_webhook")
384 .webhook("https://webhook.example.com")
385 .webhook_method("POST")
386 .webhook_secret(SecretValue::Plain(SecretString::new(
387 "secret123".to_string(),
388 )))
389 .webhook_headers(headers.clone())
390 .message("Custom Alert", "Something happened!")
391 .build();
392
393 assert_eq!(trigger.name, "my_webhook");
394 assert_eq!(trigger.trigger_type, TriggerType::Webhook);
395
396 match trigger.config {
397 TriggerTypeConfig::Webhook {
398 url,
399 method,
400 secret,
401 headers: h,
402 message,
403 payload_mode,
404 retry_policy: _,
405 } => {
406 assert_eq!(url.as_ref().to_string(), "https://webhook.example.com");
407 assert_eq!(method, Some("POST".to_string()));
408 assert_eq!(
409 secret.as_ref().map(|s| s.as_ref().to_string()),
410 Some("secret123".to_string())
411 );
412 assert_eq!(h, Some(headers));
413 assert_eq!(message.title, "Custom Alert");
414 assert_eq!(message.body, "Something happened!");
415 assert_eq!(payload_mode, WebhookPayloadMode::default());
416 }
417 _ => panic!("Expected webhook config"),
418 }
419 }
420
421 #[test]
422 fn test_slack_trigger() {
423 let trigger = TriggerBuilder::new()
424 .name("slack_alert")
425 .slack("https://slack.webhook.com")
426 .message("Alert", "Test message")
427 .build();
428
429 assert_eq!(trigger.trigger_type, TriggerType::Slack);
430 match trigger.config {
431 TriggerTypeConfig::Slack {
432 slack_url,
433 message,
434 retry_policy: _,
435 } => {
436 assert_eq!(slack_url.as_ref().to_string(), "https://slack.webhook.com");
437 assert_eq!(message.title, "Alert");
438 assert_eq!(message.body, "Test message");
439 }
440 _ => panic!("Expected slack config"),
441 }
442 }
443
444 #[test]
445 fn test_discord_trigger() {
446 let trigger = TriggerBuilder::new()
447 .name("discord_alert")
448 .discord("https://discord.webhook.com")
449 .message("Alert", "Test message")
450 .build();
451
452 assert_eq!(trigger.trigger_type, TriggerType::Discord);
453 match trigger.config {
454 TriggerTypeConfig::Discord {
455 discord_url,
456 message,
457 retry_policy: _,
458 } => {
459 assert_eq!(
460 discord_url.as_ref().to_string(),
461 "https://discord.webhook.com"
462 );
463 assert_eq!(message.title, "Alert");
464 assert_eq!(message.body, "Test message");
465 }
466 _ => panic!("Expected discord config"),
467 }
468 }
469
470 #[test]
471 fn test_script_trigger() {
472 let trigger = TriggerBuilder::new()
473 .name("script_trigger")
474 .script("test.py", ScriptLanguage::Python)
475 .build();
476
477 assert_eq!(trigger.trigger_type, TriggerType::Script);
478 match trigger.config {
479 TriggerTypeConfig::Script {
480 script_path,
481 language,
482 timeout_ms,
483 ..
484 } => {
485 assert_eq!(script_path, "test.py");
486 assert_eq!(language, ScriptLanguage::Python);
487 assert_eq!(timeout_ms, 1000);
488 }
489 _ => panic!("Expected script config"),
490 }
491 }
492
493 #[test]
494 fn test_script_trigger_with_arguments() {
495 let trigger = TriggerBuilder::new()
496 .name("script_trigger")
497 .script("test.py", ScriptLanguage::Python)
498 .script_arguments(vec!["arg1".to_string()])
499 .build();
500
501 assert_eq!(trigger.trigger_type, TriggerType::Script);
502 match trigger.config {
503 TriggerTypeConfig::Script { arguments, .. } => {
504 assert_eq!(arguments, Some(vec!["arg1".to_string()]));
505 }
506 _ => panic!("Expected script config"),
507 }
508 }
509
510 #[test]
511 fn test_script_trigger_with_timeout() {
512 let trigger = TriggerBuilder::new()
513 .name("script_trigger")
514 .script("test.py", ScriptLanguage::Python)
515 .script_timeout_ms(2000)
516 .build();
517
518 assert_eq!(trigger.trigger_type, TriggerType::Script);
519 match trigger.config {
520 TriggerTypeConfig::Script { timeout_ms, .. } => {
521 assert_eq!(timeout_ms, 2000);
522 }
523 _ => panic!("Expected script config"),
524 }
525 }
526
527 #[test]
528 fn test_telegram_trigger() {
529 let trigger = TriggerBuilder::new()
530 .name("telegram_alert")
531 .telegram(
532 "1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ123456789", "1234567890",
534 false,
535 )
536 .message("Alert", "Test message")
537 .build();
538
539 assert_eq!(trigger.trigger_type, TriggerType::Telegram);
540 match trigger.config {
541 TriggerTypeConfig::Telegram {
542 token,
543 chat_id,
544 message,
545 ..
546 } => {
547 assert_eq!(
548 token.as_ref().to_string(),
549 "1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ123456789".to_string() );
551 assert_eq!(chat_id, "1234567890");
552 assert_eq!(message.title, "Alert");
553 assert_eq!(message.body, "Test message");
554 }
555 _ => panic!("Expected telegram config"),
556 }
557 }
558
559 #[test]
560 fn test_email_trigger() {
561 let trigger = TriggerBuilder::new()
562 .name("email_alert")
563 .email(
564 "smtp.example.com",
565 "user",
566 "pass",
567 "sender@example.com",
568 vec!["recipient@example.com"],
569 )
570 .email_port(465)
571 .email_subject("Custom Subject")
572 .build();
573
574 assert_eq!(trigger.trigger_type, TriggerType::Email);
575 match trigger.config {
576 TriggerTypeConfig::Email {
577 host,
578 port,
579 username,
580 password,
581 message,
582 sender,
583 recipients,
584 ..
585 } => {
586 assert_eq!(host, "smtp.example.com");
587 assert_eq!(port, Some(465));
588 assert_eq!(username.as_ref().to_string(), "user");
589 assert_eq!(password.as_ref().to_string(), "pass");
590 assert_eq!(message.title, "Custom Subject");
591 assert_eq!(sender.as_str(), "sender@example.com");
592 assert_eq!(recipients.len(), 1);
593 assert_eq!(recipients[0].as_str(), "recipient@example.com");
594 }
595 _ => panic!("Expected email config"),
596 }
597 }
598
599 #[test]
600 fn test_telegram_token() {
601 let token = SecretValue::Environment("TELEGRAM_TOKEN".to_string());
602 let trigger = TriggerBuilder::new()
603 .name("telegram_alert")
604 .telegram("dummy_token", "1234567890", false)
605 .telegram_token(token.clone())
606 .build();
607
608 assert_eq!(trigger.trigger_type, TriggerType::Telegram);
609 match trigger.config {
610 TriggerTypeConfig::Telegram { token: t, .. } => {
611 assert_eq!(t, token);
612 }
613 _ => panic!("Expected telegram config"),
614 }
615 }
616
617 #[test]
618 fn test_email_username() {
619 let username = SecretValue::Environment("SMTP_USERNAME".to_string());
620 let trigger = TriggerBuilder::new()
621 .name("email_alert")
622 .email(
623 "smtp.example.com",
624 "dummy_user",
625 "pass",
626 "sender@example.com",
627 vec!["recipient@example.com"],
628 )
629 .email_username(username.clone())
630 .build();
631
632 assert_eq!(trigger.trigger_type, TriggerType::Email);
633 match trigger.config {
634 TriggerTypeConfig::Email { username: u, .. } => {
635 assert_eq!(u, username);
636 }
637 _ => panic!("Expected email config"),
638 }
639 }
640
641 #[test]
642 fn test_email_password() {
643 let password = SecretValue::Environment("SMTP_PASSWORD".to_string());
644 let trigger = TriggerBuilder::new()
645 .name("email_alert")
646 .email(
647 "smtp.example.com",
648 "user",
649 "dummy_pass",
650 "sender@example.com",
651 vec!["recipient@example.com"],
652 )
653 .email_password(password.clone())
654 .build();
655
656 assert_eq!(trigger.trigger_type, TriggerType::Email);
657 match trigger.config {
658 TriggerTypeConfig::Email { password: p, .. } => {
659 assert_eq!(p, password);
660 }
661 _ => panic!("Expected email config"),
662 }
663 }
664
665 #[test]
666 fn test_url() {
667 let url = SecretValue::Environment("WEBHOOK_URL".to_string());
668
669 let webhook_trigger = TriggerBuilder::new()
671 .name("webhook_alert")
672 .webhook("dummy_url")
673 .url(url.clone())
674 .build();
675
676 assert_eq!(webhook_trigger.trigger_type, TriggerType::Webhook);
677 match webhook_trigger.config {
678 TriggerTypeConfig::Webhook { url: u, .. } => {
679 assert_eq!(u, url);
680 }
681 _ => panic!("Expected webhook config"),
682 }
683
684 let discord_trigger = TriggerBuilder::new()
686 .name("discord_alert")
687 .discord("dummy_url")
688 .url(url.clone())
689 .build();
690
691 assert_eq!(discord_trigger.trigger_type, TriggerType::Discord);
692 match discord_trigger.config {
693 TriggerTypeConfig::Discord { discord_url: u, .. } => {
694 assert_eq!(u, url);
695 }
696 _ => panic!("Expected discord config"),
697 }
698
699 let slack_trigger = TriggerBuilder::new()
701 .name("slack_alert")
702 .slack("dummy_url")
703 .url(url.clone())
704 .build();
705
706 assert_eq!(slack_trigger.trigger_type, TriggerType::Slack);
707 match slack_trigger.config {
708 TriggerTypeConfig::Slack { slack_url: u, .. } => {
709 assert_eq!(u, url);
710 }
711 _ => panic!("Expected slack config"),
712 }
713 }
714}