openzeppelin_monitor/utils/tests/builders/
trigger.rs

1//! Test helper utilities for Trigger configuration
2//!
3//! - `TriggerBuilder`: Builder for creating test Trigger instances
4
5use crate::{
6	models::{
7		NotificationMessage, ScriptLanguage, SecretString, SecretValue, Trigger, TriggerType,
8		TriggerTypeConfig, WebhookPayloadMode,
9	},
10	utils::RetryConfig,
11};
12use email_address::EmailAddress;
13
14/// Builder for creating test Trigger instances
15pub 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", // noboost
533				"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() // noboost
550				);
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		// Test with webhook
670		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		// Test with discord
685		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		// Test with slack
700		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}