1use async_trait::async_trait;
7
8use std::{collections::HashMap, sync::Arc};
9
10mod email;
11mod error;
12pub mod payload_builder;
13mod pool;
14mod script;
15mod template_formatter;
16mod webhook;
17
18use crate::{
19 models::{
20 MonitorMatch, NotificationMessage, ScriptLanguage, Trigger, TriggerType, TriggerTypeConfig,
21 WebhookPayloadMode,
22 },
23 utils::{normalize_string, RetryConfig},
24};
25
26pub use email::{EmailContent, EmailNotifier, SmtpConfig};
27pub use error::NotificationError;
28pub use payload_builder::{
29 DiscordPayloadBuilder, GenericWebhookPayloadBuilder, SlackPayloadBuilder,
30 TelegramPayloadBuilder, WebhookPayloadBuilder,
31};
32pub use pool::NotificationClientPool;
33pub use script::ScriptNotifier;
34pub use webhook::{WebhookConfig, WebhookNotifier};
35
36struct WebhookComponents {
38 config: WebhookConfig,
39 retry_policy: RetryConfig,
40 builder: Box<dyn WebhookPayloadBuilder>,
41}
42
43type WebhookParts = (
45 String, NotificationMessage, Option<String>, Option<String>, Option<HashMap<String, String>>, Box<dyn WebhookPayloadBuilder>, );
52
53trait AsWebhookComponents {
56 fn as_webhook_components(&self) -> Result<WebhookComponents, NotificationError>;
60}
61
62impl AsWebhookComponents for TriggerTypeConfig {
63 fn as_webhook_components(&self) -> Result<WebhookComponents, NotificationError> {
64 let (url, message, method, secret, headers, builder): WebhookParts = match self {
65 TriggerTypeConfig::Webhook {
66 url,
67 message,
68 method,
69 secret,
70 headers,
71 ..
72 } => (
73 url.as_ref().to_string(),
74 message.clone(),
75 method.clone(),
76 secret.as_ref().map(|s| s.as_ref().to_string()),
77 headers.clone(),
78 Box::new(GenericWebhookPayloadBuilder),
79 ),
80 TriggerTypeConfig::Discord {
81 discord_url,
82 message,
83 ..
84 } => (
85 discord_url.as_ref().to_string(),
86 message.clone(),
87 Some("POST".to_string()),
88 None,
89 None,
90 Box::new(DiscordPayloadBuilder),
91 ),
92 TriggerTypeConfig::Telegram {
93 token,
94 message,
95 chat_id,
96 disable_web_preview,
97 ..
98 } => (
99 format!("https://api.telegram.org/bot{}/sendMessage", token.as_ref()),
100 message.clone(),
101 Some("POST".to_string()),
102 None,
103 None,
104 Box::new(TelegramPayloadBuilder {
105 chat_id: chat_id.clone(),
106 disable_web_preview: disable_web_preview.unwrap_or(false),
107 }),
108 ),
109 TriggerTypeConfig::Slack {
110 slack_url, message, ..
111 } => (
112 slack_url.as_ref().to_string(),
113 message.clone(),
114 Some("POST".to_string()),
115 None,
116 None,
117 Box::new(SlackPayloadBuilder),
118 ),
119 _ => {
120 return Err(NotificationError::config_error(
121 format!("Trigger type is not webhook-compatible: {:?}", self),
122 None,
123 None,
124 ))
125 }
126 };
127
128 let config = WebhookConfig {
130 url,
131 title: message.title,
132 body_template: message.body,
133 method,
134 secret,
135 headers,
136 url_params: None,
137 payload_fields: None,
138 };
139
140 let retry_policy = self.get_retry_policy().ok_or_else(|| {
142 NotificationError::config_error(
143 "Webhook trigger config is unexpectedly missing a retry policy.",
144 None,
145 None,
146 )
147 })?;
148
149 Ok(WebhookComponents {
150 config,
151 retry_policy,
152 builder,
153 })
154 }
155}
156
157#[async_trait]
162pub trait ScriptExecutor {
163 async fn script_notify(
172 &self,
173 monitor_match: &MonitorMatch,
174 script_content: &(ScriptLanguage, String),
175 ) -> Result<(), NotificationError>;
176}
177
178pub struct NotificationService {
180 client_pool: Arc<NotificationClientPool>,
182}
183
184impl NotificationService {
185 pub fn new() -> Self {
187 NotificationService {
188 client_pool: Arc::new(NotificationClientPool::new()),
189 }
190 }
191
192 pub async fn execute(
204 &self,
205 trigger: &Trigger,
206 variables: &HashMap<String, String>,
207 monitor_match: &MonitorMatch,
208 trigger_scripts: &HashMap<String, (ScriptLanguage, String)>,
209 ) -> Result<(), NotificationError> {
210 match &trigger.trigger_type {
211 TriggerType::Slack
213 | TriggerType::Discord
214 | TriggerType::Webhook
215 | TriggerType::Telegram => {
216 let is_raw_mode = matches!(
218 &trigger.config,
219 TriggerTypeConfig::Webhook {
220 payload_mode: WebhookPayloadMode::Raw,
221 ..
222 }
223 );
224
225 let components = trigger.config.as_webhook_components()?;
227
228 let http_client = self
230 .client_pool
231 .get_or_create_http_client(&components.retry_policy)
232 .await
233 .map_err(|e| {
234 NotificationError::execution_error(
235 "Failed to get or create HTTP client from pool".to_string(),
236 Some(e.into()),
237 None,
238 )
239 })?;
240
241 let payload = if is_raw_mode {
243 serde_json::to_value(monitor_match).map_err(|e| {
245 NotificationError::internal_error(
246 format!("Failed to serialize MonitorMatch: {}", e),
247 Some(e.into()),
248 None,
249 )
250 })?
251 } else {
252 components.builder.build_payload(
254 &components.config.title,
255 &components.config.body_template,
256 variables,
257 )
258 };
259
260 let notifier = WebhookNotifier::new(components.config, http_client)?;
262
263 notifier.notify_json(&payload).await?;
264 }
265 TriggerType::Email => {
266 let smtp_config = match &trigger.config {
268 TriggerTypeConfig::Email {
269 host,
270 port,
271 username,
272 password,
273 ..
274 } => SmtpConfig {
275 host: host.clone(),
276 port: port.unwrap_or(465),
277 username: username.as_ref().to_string(),
278 password: password.as_ref().to_string(),
279 },
280 _ => {
281 return Err(NotificationError::config_error(
282 "Invalid email configuration".to_string(),
283 None,
284 None,
285 ));
286 }
287 };
288
289 let smtp_client = self
291 .client_pool
292 .get_or_create_smtp_client(&smtp_config)
293 .await
294 .map_err(|e| {
295 NotificationError::execution_error(
296 "Failed to get SMTP client from pool".to_string(),
297 Some(e.into()),
298 None,
299 )
300 })?;
301
302 let notifier = EmailNotifier::from_config(&trigger.config, smtp_client)?;
303 let message = EmailNotifier::format_message(notifier.body_template(), variables);
304 notifier.notify(&message).await?;
305 }
306 TriggerType::Script => {
307 let notifier = ScriptNotifier::from_config(&trigger.config)?;
308 let monitor_name = match monitor_match {
309 MonitorMatch::EVM(evm_match) => &evm_match.monitor.name,
310 MonitorMatch::Stellar(stellar_match) => &stellar_match.monitor.name,
311 MonitorMatch::Midnight(midnight_match) => &midnight_match.monitor.name,
312 MonitorMatch::Solana(solana_match) => &solana_match.monitor.name,
313 };
314 let script_path = match &trigger.config {
315 TriggerTypeConfig::Script { script_path, .. } => script_path,
316 _ => {
317 return Err(NotificationError::config_error(
318 "Invalid script configuration".to_string(),
319 None,
320 None,
321 ));
322 }
323 };
324 let script = trigger_scripts
325 .get(&format!(
326 "{}|{}",
327 normalize_string(monitor_name),
328 script_path
329 ))
330 .ok_or_else(|| {
331 NotificationError::config_error(
332 "Script content not found".to_string(),
333 None,
334 None,
335 )
336 });
337 let script_content = match &script {
338 Ok(content) => content,
339 Err(e) => {
340 return Err(NotificationError::config_error(e.to_string(), None, None));
341 }
342 };
343
344 notifier
345 .script_notify(monitor_match, script_content)
346 .await?;
347 }
348 }
349 Ok(())
350 }
351}
352
353impl Default for NotificationService {
354 fn default() -> Self {
355 Self::new()
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use crate::{
363 models::{
364 AddressWithSpec, EVMMonitorMatch, EVMTransactionReceipt, EventCondition,
365 FunctionCondition, MatchConditions, Monitor, MonitorMatch, NotificationMessage,
366 ScriptLanguage, SecretString, SecretValue, SolanaMonitorMatch, TransactionCondition,
367 TriggerType,
368 },
369 utils::tests::builders::{
370 evm::monitor::MonitorBuilder as EVMMonitorBuilder,
371 evm::transaction::TransactionBuilder as EVMTransactionBuilder,
372 solana::block::BlockBuilder as SolanaBlockBuilder,
373 solana::monitor::MonitorBuilder as SolanaMonitorBuilder,
374 solana::transaction::TransactionBuilder as SolanaTransactionBuilder,
375 trigger::TriggerBuilder,
376 },
377 };
378 use std::collections::HashMap;
379
380 fn create_test_evm_monitor(
381 event_conditions: Vec<EventCondition>,
382 function_conditions: Vec<FunctionCondition>,
383 transaction_conditions: Vec<TransactionCondition>,
384 addresses: Vec<AddressWithSpec>,
385 ) -> Monitor {
386 let mut builder = EVMMonitorBuilder::new()
387 .name("test")
388 .networks(vec!["evm_mainnet".to_string()]);
389
390 for event in event_conditions {
392 builder = builder.event(&event.signature, event.expression);
393 }
394 for function in function_conditions {
395 builder = builder.function(&function.signature, function.expression);
396 }
397 for transaction in transaction_conditions {
398 builder = builder.transaction(transaction.status, transaction.expression);
399 }
400
401 for addr in addresses {
403 builder = builder.address(&addr.address);
404 }
405
406 builder.build()
407 }
408
409 fn create_test_solana_monitor(
410 event_conditions: Vec<EventCondition>,
411 function_conditions: Vec<FunctionCondition>,
412 transaction_conditions: Vec<TransactionCondition>,
413 addresses: Vec<AddressWithSpec>,
414 ) -> Monitor {
415 let mut builder = SolanaMonitorBuilder::new()
416 .name("test_solana")
417 .networks(vec!["solana_mainnet".to_string()]);
418
419 for event in event_conditions {
421 builder = builder.event(&event.signature, event.expression);
422 }
423 for function in function_conditions {
424 builder = builder.function(&function.signature, function.expression);
425 }
426 for transaction in transaction_conditions {
427 builder = builder.transaction(transaction.status, transaction.expression);
428 }
429
430 for addr in addresses {
432 builder = builder.address(&addr.address);
433 }
434
435 builder.build()
436 }
437
438 fn create_mock_evm_monitor_match() -> MonitorMatch {
439 MonitorMatch::EVM(Box::new(EVMMonitorMatch {
440 monitor: create_test_evm_monitor(vec![], vec![], vec![], vec![]),
441 transaction: EVMTransactionBuilder::new().build(),
442 receipt: Some(EVMTransactionReceipt::default()),
443 logs: Some(vec![]),
444 network_slug: "evm_mainnet".to_string(),
445 matched_on: MatchConditions {
446 functions: vec![],
447 events: vec![],
448 transactions: vec![],
449 },
450 matched_on_args: None,
451 }))
452 }
453
454 fn create_mock_solana_monitor_match() -> MonitorMatch {
455 MonitorMatch::Solana(Box::new(SolanaMonitorMatch {
456 monitor: create_test_solana_monitor(vec![], vec![], vec![], vec![]),
457 transaction: SolanaTransactionBuilder::new().build(),
458 block: SolanaBlockBuilder::new().build(),
459 network_slug: "solana_mainnet".to_string(),
460 matched_on: MatchConditions {
461 functions: vec![],
462 events: vec![],
463 transactions: vec![],
464 },
465 matched_on_args: None,
466 }))
467 }
468
469 fn create_mock_monitor_match() -> MonitorMatch {
471 create_mock_evm_monitor_match()
472 }
473
474 #[tokio::test]
475 async fn test_slack_notification_invalid_config() {
476 let service = NotificationService::new();
477
478 let trigger = TriggerBuilder::new()
479 .name("test_slack")
480 .script("invalid", ScriptLanguage::Python)
481 .trigger_type(TriggerType::Slack) .build();
483
484 let variables = HashMap::new();
485 let result = service
486 .execute(
487 &trigger,
488 &variables,
489 &create_mock_monitor_match(),
490 &HashMap::new(),
491 )
492 .await;
493 assert!(result.is_err());
494 match result {
495 Err(NotificationError::ConfigError(ctx)) => {
496 assert!(ctx
497 .message
498 .contains("Trigger type is not webhook-compatible"));
499 }
500 _ => panic!("Expected ConfigError"),
501 }
502 }
503
504 #[tokio::test]
505 async fn test_email_notification_invalid_config() {
506 let service = NotificationService::new();
507
508 let trigger = TriggerBuilder::new()
509 .name("test_email")
510 .script("invalid", ScriptLanguage::Python)
511 .trigger_type(TriggerType::Email) .build();
513
514 let variables = HashMap::new();
515 let result = service
516 .execute(
517 &trigger,
518 &variables,
519 &create_mock_monitor_match(),
520 &HashMap::new(),
521 )
522 .await;
523 assert!(result.is_err());
524 match result {
525 Err(NotificationError::ConfigError(ctx)) => {
526 assert!(ctx.message.contains("Invalid email configuration"));
527 }
528 _ => panic!("Expected ConfigError"),
529 }
530 }
531
532 #[tokio::test]
533 async fn test_webhook_notification_invalid_config() {
534 let service = NotificationService::new();
535
536 let trigger = TriggerBuilder::new()
537 .name("test_webhook")
538 .script("invalid", ScriptLanguage::Python)
539 .trigger_type(TriggerType::Webhook) .build();
541
542 let variables = HashMap::new();
543 let result = service
544 .execute(
545 &trigger,
546 &variables,
547 &create_mock_monitor_match(),
548 &HashMap::new(),
549 )
550 .await;
551 assert!(result.is_err());
552 match result {
553 Err(NotificationError::ConfigError(ctx)) => {
554 assert!(ctx
555 .message
556 .contains("Trigger type is not webhook-compatible"));
557 }
558 _ => panic!("Expected ConfigError"),
559 }
560 }
561
562 #[tokio::test]
563 async fn test_discord_notification_invalid_config() {
564 let service = NotificationService::new();
565
566 let trigger = TriggerBuilder::new()
567 .name("test_discord")
568 .script("invalid", ScriptLanguage::Python)
569 .trigger_type(TriggerType::Discord) .build();
571
572 let variables = HashMap::new();
573 let result = service
574 .execute(
575 &trigger,
576 &variables,
577 &create_mock_monitor_match(),
578 &HashMap::new(),
579 )
580 .await;
581 assert!(result.is_err());
582 match result {
583 Err(NotificationError::ConfigError(ctx)) => {
584 assert!(ctx
585 .message
586 .contains("Trigger type is not webhook-compatible"));
587 }
588 _ => panic!("Expected ConfigError"),
589 }
590 }
591
592 #[tokio::test]
593 async fn test_telegram_notification_invalid_config() {
594 let service = NotificationService::new();
595
596 let trigger = TriggerBuilder::new()
597 .name("test_telegram")
598 .script("invalid", ScriptLanguage::Python)
599 .trigger_type(TriggerType::Telegram) .build();
601
602 let variables = HashMap::new();
603 let result = service
604 .execute(
605 &trigger,
606 &variables,
607 &create_mock_monitor_match(),
608 &HashMap::new(),
609 )
610 .await;
611 assert!(result.is_err());
612 match result {
613 Err(NotificationError::ConfigError(ctx)) => {
614 assert!(ctx
615 .message
616 .contains("Trigger type is not webhook-compatible"));
617 }
618 _ => panic!("Expected ConfigError"),
619 }
620 }
621
622 #[tokio::test]
623 async fn test_script_notification_invalid_config() {
624 let service = NotificationService::new();
625
626 let trigger = TriggerBuilder::new()
627 .name("test_script")
628 .telegram("invalid", "invalid", false)
629 .trigger_type(TriggerType::Script) .build();
631
632 let variables = HashMap::new();
633
634 let result = service
635 .execute(
636 &trigger,
637 &variables,
638 &create_mock_monitor_match(),
639 &HashMap::new(),
640 )
641 .await;
642
643 assert!(result.is_err());
644 match result {
645 Err(NotificationError::ConfigError(ctx)) => {
646 assert!(ctx.message.contains("Invalid script configuration"));
647 }
648 _ => panic!("Expected ConfigError"),
649 }
650 }
651
652 #[test]
653 fn as_webhook_components_trait_for_slack_config() {
654 let title = "Slack Title";
655 let message = "Slack Body";
656
657 let slack_config = TriggerTypeConfig::Slack {
658 slack_url: SecretValue::Plain(SecretString::new(
659 "https://slack.example.com".to_string(),
660 )),
661 message: NotificationMessage {
662 title: title.to_string(),
663 body: message.to_string(),
664 },
665 retry_policy: RetryConfig::default(),
666 };
667
668 let components = slack_config.as_webhook_components().unwrap();
669
670 assert_eq!(components.config.url, "https://slack.example.com");
672 assert_eq!(components.config.title, title);
673 assert_eq!(components.config.body_template, message);
674 assert_eq!(components.config.method, Some("POST".to_string()));
675 assert!(components.config.secret.is_none());
676
677 let payload = components
679 .builder
680 .build_payload(title, message, &HashMap::new());
681 assert!(
682 payload.get("blocks").is_some(),
683 "Expected a Slack payload with 'blocks'"
684 );
685 assert!(
686 payload.get("content").is_none(),
687 "Did not expect a Discord payload"
688 );
689 }
690
691 #[test]
692 fn as_webhook_components_trait_for_discord_config() {
693 let title = "Discord Title";
694 let message = "Discord Body";
695 let discord_config = TriggerTypeConfig::Discord {
696 discord_url: SecretValue::Plain(SecretString::new(
697 "https://discord.example.com".to_string(),
698 )),
699 message: NotificationMessage {
700 title: title.to_string(),
701 body: message.to_string(),
702 },
703 retry_policy: RetryConfig::default(),
704 };
705
706 let components = discord_config.as_webhook_components().unwrap();
707
708 assert_eq!(components.config.url, "https://discord.example.com");
710 assert_eq!(components.config.title, title);
711 assert_eq!(components.config.body_template, message);
712 assert_eq!(components.config.method, Some("POST".to_string()));
713
714 let payload = components
716 .builder
717 .build_payload(title, message, &HashMap::new());
718 assert!(
719 payload.get("content").is_some(),
720 "Expected a Discord payload with 'content'"
721 );
722 assert!(
723 payload.get("blocks").is_none(),
724 "Did not expect a Slack payload"
725 );
726 }
727
728 #[test]
729 fn as_webhook_components_trait_for_telegram_config() {
730 let title = "Telegram Title";
731 let message = "Telegram Body";
732 let telegram_config = TriggerTypeConfig::Telegram {
733 token: SecretValue::Plain(SecretString::new("test-token".to_string())),
734 chat_id: "12345".to_string(),
735 disable_web_preview: Some(true),
736 message: NotificationMessage {
737 title: title.to_string(),
738 body: message.to_string(),
739 },
740 retry_policy: RetryConfig::default(),
741 };
742
743 let components = telegram_config.as_webhook_components().unwrap();
744
745 assert_eq!(
747 components.config.url,
748 "https://api.telegram.org/bottest-token/sendMessage"
749 );
750 assert_eq!(components.config.title, title);
751 assert_eq!(components.config.body_template, message);
752
753 let payload = components
755 .builder
756 .build_payload(title, message, &HashMap::new());
757 assert_eq!(payload.get("chat_id").unwrap(), "12345");
758 assert_eq!(payload.get("disable_web_page_preview").unwrap(), &true);
759 assert!(payload.get("text").is_some());
760 }
761
762 #[test]
763 fn as_webhook_components_trait_for_generic_webhook_config() {
764 let title = "Generic Title";
765 let body_template = "Generic Body";
766 let webhook_config = TriggerTypeConfig::Webhook {
767 url: SecretValue::Plain(SecretString::new("https://generic.example.com".to_string())),
768 message: NotificationMessage {
769 title: title.to_string(),
770 body: body_template.to_string(),
771 },
772 method: Some("PUT".to_string()),
773 secret: Some(SecretValue::Plain(SecretString::new(
774 "my-secret".to_string(),
775 ))),
776 headers: Some([("X-Custom".to_string(), "Value".to_string())].into()),
777 payload_mode: WebhookPayloadMode::default(),
778 retry_policy: RetryConfig::default(),
779 };
780
781 let components = webhook_config.as_webhook_components().unwrap();
782
783 assert_eq!(components.config.url, "https://generic.example.com");
785 assert_eq!(components.config.method, Some("PUT".to_string()));
786 assert_eq!(components.config.secret, Some("my-secret".to_string()));
787 assert!(components.config.headers.is_some());
788 assert_eq!(
789 components.config.headers.unwrap().get("X-Custom").unwrap(),
790 "Value"
791 );
792
793 let payload = components
795 .builder
796 .build_payload(title, body_template, &HashMap::new());
797 assert!(payload.get("title").is_some());
798 assert!(payload.get("body").is_some());
799 }
800
801 #[tokio::test]
806 async fn test_slack_notification_invalid_config_solana() {
807 let service = NotificationService::new();
808
809 let trigger = TriggerBuilder::new()
810 .name("test_slack_solana")
811 .script("invalid", ScriptLanguage::Python)
812 .trigger_type(TriggerType::Slack) .build();
814
815 let variables = HashMap::new();
816 let result = service
817 .execute(
818 &trigger,
819 &variables,
820 &create_mock_solana_monitor_match(),
821 &HashMap::new(),
822 )
823 .await;
824 assert!(result.is_err());
825 match result {
826 Err(NotificationError::ConfigError(ctx)) => {
827 assert!(ctx
828 .message
829 .contains("Trigger type is not webhook-compatible"));
830 }
831 _ => panic!("Expected ConfigError"),
832 }
833 }
834
835 #[tokio::test]
836 async fn test_email_notification_invalid_config_solana() {
837 let service = NotificationService::new();
838
839 let trigger = TriggerBuilder::new()
840 .name("test_email_solana")
841 .script("invalid", ScriptLanguage::Python)
842 .trigger_type(TriggerType::Email) .build();
844
845 let variables = HashMap::new();
846 let result = service
847 .execute(
848 &trigger,
849 &variables,
850 &create_mock_solana_monitor_match(),
851 &HashMap::new(),
852 )
853 .await;
854 assert!(result.is_err());
855 match result {
856 Err(NotificationError::ConfigError(ctx)) => {
857 assert!(ctx.message.contains("Invalid email configuration"));
858 }
859 _ => panic!("Expected ConfigError"),
860 }
861 }
862
863 #[tokio::test]
864 async fn test_webhook_notification_invalid_config_solana() {
865 let service = NotificationService::new();
866
867 let trigger = TriggerBuilder::new()
868 .name("test_webhook_solana")
869 .script("invalid", ScriptLanguage::Python)
870 .trigger_type(TriggerType::Webhook) .build();
872
873 let variables = HashMap::new();
874 let result = service
875 .execute(
876 &trigger,
877 &variables,
878 &create_mock_solana_monitor_match(),
879 &HashMap::new(),
880 )
881 .await;
882 assert!(result.is_err());
883 match result {
884 Err(NotificationError::ConfigError(ctx)) => {
885 assert!(ctx
886 .message
887 .contains("Trigger type is not webhook-compatible"));
888 }
889 _ => panic!("Expected ConfigError"),
890 }
891 }
892
893 #[tokio::test]
894 async fn test_discord_notification_invalid_config_solana() {
895 let service = NotificationService::new();
896
897 let trigger = TriggerBuilder::new()
898 .name("test_discord_solana")
899 .script("invalid", ScriptLanguage::Python)
900 .trigger_type(TriggerType::Discord) .build();
902
903 let variables = HashMap::new();
904 let result = service
905 .execute(
906 &trigger,
907 &variables,
908 &create_mock_solana_monitor_match(),
909 &HashMap::new(),
910 )
911 .await;
912 assert!(result.is_err());
913 match result {
914 Err(NotificationError::ConfigError(ctx)) => {
915 assert!(ctx
916 .message
917 .contains("Trigger type is not webhook-compatible"));
918 }
919 _ => panic!("Expected ConfigError"),
920 }
921 }
922
923 #[tokio::test]
924 async fn test_telegram_notification_invalid_config_solana() {
925 let service = NotificationService::new();
926
927 let trigger = TriggerBuilder::new()
928 .name("test_telegram_solana")
929 .script("invalid", ScriptLanguage::Python)
930 .trigger_type(TriggerType::Telegram) .build();
932
933 let variables = HashMap::new();
934 let result = service
935 .execute(
936 &trigger,
937 &variables,
938 &create_mock_solana_monitor_match(),
939 &HashMap::new(),
940 )
941 .await;
942 assert!(result.is_err());
943 match result {
944 Err(NotificationError::ConfigError(ctx)) => {
945 assert!(ctx
946 .message
947 .contains("Trigger type is not webhook-compatible"));
948 }
949 _ => panic!("Expected ConfigError"),
950 }
951 }
952
953 #[tokio::test]
954 async fn test_script_notification_invalid_config_solana() {
955 let service = NotificationService::new();
956
957 let trigger = TriggerBuilder::new()
958 .name("test_script_solana")
959 .telegram("invalid", "invalid", false)
960 .trigger_type(TriggerType::Script) .build();
962
963 let variables = HashMap::new();
964
965 let result = service
966 .execute(
967 &trigger,
968 &variables,
969 &create_mock_solana_monitor_match(),
970 &HashMap::new(),
971 )
972 .await;
973
974 assert!(result.is_err());
975 match result {
976 Err(NotificationError::ConfigError(ctx)) => {
977 assert!(ctx.message.contains("Invalid script configuration"));
978 }
979 _ => panic!("Expected ConfigError"),
980 }
981 }
982}