1use parking_lot::Mutex;
34use std::sync::Arc;
35use thiserror::Error;
36
37#[derive(Debug, Error)]
43pub enum NotifyError {
44 #[error("通知字段缺失: {0}")]
46 MissingField(String),
47 #[error("通知发送失败: {0}")]
49 SendFailed(String),
50 #[error("HTTP 传输失败: {0}")]
52 HttpTransport(String),
53 #[error("序列化失败: {0}")]
55 Serialize(String),
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
66#[repr(u8)]
67pub enum NotifyLevel {
68 #[default]
70 Info = 0,
71 Warning = 1,
73 Error = 2,
75 Critical = 3,
77}
78
79impl NotifyLevel {
80 pub fn slack_color(self) -> &'static str {
87 match self {
88 Self::Info => "#36a64f",
89 Self::Warning => "#ffcc00",
90 Self::Error => "#ff0000",
91 Self::Critical => "#b22222",
92 }
93 }
94
95 pub fn as_str(self) -> &'static str {
97 match self {
98 Self::Info => "info",
99 Self::Warning => "warning",
100 Self::Error => "error",
101 Self::Critical => "critical",
102 }
103 }
104}
105
106impl std::fmt::Display for NotifyLevel {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.write_str(self.as_str())
109 }
110}
111
112impl std::str::FromStr for NotifyLevel {
113 type Err = NotifyError;
114
115 fn from_str(s: &str) -> Result<Self, Self::Err> {
116 match s.to_lowercase().as_str() {
117 "info" => Ok(Self::Info),
118 "warning" | "warn" => Ok(Self::Warning),
119 "error" | "err" => Ok(Self::Error),
120 "critical" | "crit" => Ok(Self::Critical),
121 other => Err(NotifyError::MissingField(format!("未知通知级别: {other}"))),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Default)]
160pub struct Notification {
161 pub channel: String,
163 pub title: String,
165 pub content: String,
167 pub level: NotifyLevel,
169 pub metadata: serde_json::Value,
171}
172
173impl Notification {
174 pub fn new() -> Self {
176 Self::default()
177 }
178
179 pub fn channel(mut self, channel: impl Into<String>) -> Self {
181 self.channel = channel.into();
182 self
183 }
184
185 pub fn title(mut self, title: impl Into<String>) -> Self {
187 self.title = title.into();
188 self
189 }
190
191 pub fn content(mut self, content: impl Into<String>) -> Self {
193 self.content = content.into();
194 self
195 }
196
197 pub fn level(mut self, level: NotifyLevel) -> Self {
199 self.level = level;
200 self
201 }
202
203 pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
205 self.metadata = metadata;
206 self
207 }
208
209 pub fn validate(&self) -> Result<(), NotifyError> {
217 if self.channel.is_empty() {
218 return Err(NotifyError::MissingField("channel".into()));
219 }
220 if self.title.is_empty() {
221 return Err(NotifyError::MissingField("title".into()));
222 }
223 if self.content.is_empty() {
224 return Err(NotifyError::MissingField("content".into()));
225 }
226 Ok(())
227 }
228}
229
230pub trait Notifier: Send + Sync {
247 fn send(&self, notification: Notification) -> Result<(), NotifyError>;
257}
258
259#[derive(Debug, Clone, Default)]
271pub struct MemoryNotifier {
272 sent: Arc<Mutex<Vec<Notification>>>,
274}
275
276impl MemoryNotifier {
277 pub fn new() -> Self {
279 Self::default()
280 }
281
282 pub fn count(&self) -> usize {
284 self.sent.lock().len()
285 }
286
287 pub fn all(&self) -> Vec<Notification> {
289 self.sent.lock().clone()
290 }
291
292 pub fn last(&self) -> Option<Notification> {
294 self.sent.lock().last().cloned()
295 }
296
297 pub fn clear(&self) {
299 self.sent.lock().clear();
300 }
301}
302
303impl Notifier for MemoryNotifier {
304 fn send(&self, notification: Notification) -> Result<(), NotifyError> {
305 notification.validate()?;
307
308 self.sent.lock().push(notification);
310 Ok(())
311 }
312}
313
314pub trait HttpTransport: Send + Sync {
327 fn post_json(&self, url: &str, body: &str) -> Result<(), NotifyError>;
338}
339
340#[derive(Debug, Default)]
348pub struct MemoryHttpTransport {
349 requests: Mutex<Vec<(String, String)>>,
351}
352
353impl MemoryHttpTransport {
354 pub fn new() -> Self {
356 Self::default()
357 }
358
359 pub fn count(&self) -> usize {
361 self.requests.lock().len()
362 }
363
364 pub fn all(&self) -> Vec<(String, String)> {
366 self.requests.lock().clone()
367 }
368
369 pub fn last(&self) -> Option<(String, String)> {
371 self.requests.lock().last().cloned()
372 }
373
374 pub fn clear(&self) {
376 self.requests.lock().clear();
377 }
378}
379
380impl HttpTransport for MemoryHttpTransport {
381 fn post_json(&self, url: &str, body: &str) -> Result<(), NotifyError> {
382 self.requests
383 .lock()
384 .push((url.to_string(), body.to_string()));
385 Ok(())
386 }
387}
388
389#[derive(Debug, Clone)]
397pub struct SlackConfig {
398 pub webhook_url: String,
400 pub channel: Option<String>,
402 pub username: Option<String>,
404 pub icon_emoji: Option<String>,
406}
407
408impl SlackConfig {
409 pub fn new(webhook_url: impl Into<String>) -> Self {
415 Self {
416 webhook_url: webhook_url.into(),
417 channel: None,
418 username: None,
419 icon_emoji: None,
420 }
421 }
422
423 pub fn with_channel(mut self, channel: impl Into<String>) -> Self {
425 self.channel = Some(channel.into());
426 self
427 }
428
429 pub fn with_username(mut self, username: impl Into<String>) -> Self {
431 self.username = Some(username.into());
432 self
433 }
434
435 pub fn with_icon_emoji(mut self, icon_emoji: impl Into<String>) -> Self {
437 self.icon_emoji = Some(icon_emoji.into());
438 self
439 }
440}
441
442#[derive(Debug, Clone, serde::Serialize)]
450struct SlackPayload {
451 text: String,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 channel: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 username: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
461 icon_emoji: Option<String>,
462 attachments: Vec<SlackAttachment>,
464}
465
466#[derive(Debug, Clone, serde::Serialize)]
468struct SlackAttachment {
469 color: String,
471 title: String,
473 text: String,
475 ts: i64,
477}
478
479pub struct SlackNotifier {
510 config: SlackConfig,
512 transport: Arc<dyn HttpTransport>,
514}
515
516impl SlackNotifier {
517 pub fn new(config: SlackConfig, transport: Arc<dyn HttpTransport>) -> Self {
524 Self { config, transport }
525 }
526
527 fn build_payload(&self, notification: &Notification) -> Result<String, NotifyError> {
537 let ts = chrono::Utc::now().timestamp();
538 let payload = SlackPayload {
539 text: format!(
541 "[{}] {} — {}",
542 notification.level.as_str().to_uppercase(),
543 notification.title,
544 notification.content
545 ),
546 channel: self.config.channel.clone(),
547 username: self.config.username.clone(),
548 icon_emoji: self.config.icon_emoji.clone(),
549 attachments: vec![SlackAttachment {
550 color: notification.level.slack_color().to_string(),
551 title: notification.title.clone(),
552 text: notification.content.clone(),
553 ts,
554 }],
555 };
556
557 serde_json::to_string(&payload).map_err(|e| NotifyError::Serialize(e.to_string()))
558 }
559}
560
561impl Notifier for SlackNotifier {
562 fn send(&self, notification: Notification) -> Result<(), NotifyError> {
563 notification.validate()?;
565
566 if self.config.webhook_url.is_empty() {
568 return Err(NotifyError::MissingField("webhook_url".into()));
569 }
570
571 let body = self.build_payload(¬ification)?;
573
574 self.transport
576 .post_json(&self.config.webhook_url, &body)
577 .map_err(|e| NotifyError::HttpTransport(format!("Slack Webhook 发送失败: {e}")))?;
578
579 Ok(())
580 }
581}
582
583#[derive(Debug, Clone, Default)]
615pub struct SmsMessage {
616 pub phone: String,
618 pub template_id: String,
620 pub template_params: Vec<String>,
622 pub sign_name: Option<String>,
624 pub metadata: serde_json::Value,
626}
627
628impl SmsMessage {
629 pub fn new() -> Self {
631 Self::default()
632 }
633
634 pub fn phone(mut self, phone: impl Into<String>) -> Self {
636 self.phone = phone.into();
637 self
638 }
639
640 pub fn template_id(mut self, template_id: impl Into<String>) -> Self {
642 self.template_id = template_id.into();
643 self
644 }
645
646 pub fn template_param(mut self, param: impl Into<String>) -> Self {
648 self.template_params.push(param.into());
649 self
650 }
651
652 pub fn template_params(mut self, params: Vec<String>) -> Self {
654 self.template_params = params;
655 self
656 }
657
658 pub fn sign_name(mut self, sign_name: impl Into<String>) -> Self {
660 self.sign_name = Some(sign_name.into());
661 self
662 }
663
664 pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
666 self.metadata = metadata;
667 self
668 }
669
670 pub fn validate(&self) -> Result<(), NotifyError> {
677 if self.phone.is_empty() {
678 return Err(NotifyError::MissingField("phone".into()));
679 }
680 if self.template_id.is_empty() {
681 return Err(NotifyError::MissingField("template_id".into()));
682 }
683 Ok(())
684 }
685}
686
687pub trait SmsNotifier: Send + Sync {
700 fn send_sms(&self, message: SmsMessage) -> Result<(), NotifyError>;
710}
711
712#[derive(Debug, Clone, Default)]
720pub struct MemorySmsNotifier {
721 sent: Arc<Mutex<Vec<SmsMessage>>>,
723}
724
725impl MemorySmsNotifier {
726 pub fn new() -> Self {
728 Self::default()
729 }
730
731 pub fn count(&self) -> usize {
733 self.sent.lock().len()
734 }
735
736 pub fn all(&self) -> Vec<SmsMessage> {
738 self.sent.lock().clone()
739 }
740
741 pub fn last(&self) -> Option<SmsMessage> {
743 self.sent.lock().last().cloned()
744 }
745
746 pub fn clear(&self) {
748 self.sent.lock().clear();
749 }
750}
751
752impl SmsNotifier for MemorySmsNotifier {
753 fn send_sms(&self, message: SmsMessage) -> Result<(), NotifyError> {
754 message.validate()?;
756 self.sent.lock().push(message);
758 Ok(())
759 }
760}
761
762#[derive(Debug, Clone)]
770pub struct TencentSmsConfig {
771 pub secret_id: String,
773 pub secret_key: String,
775 pub app_id: String,
777 pub default_sign_name: Option<String>,
779 pub region: String,
781 pub endpoint: String,
783}
784
785impl TencentSmsConfig {
786 pub fn new(
794 secret_id: impl Into<String>,
795 secret_key: impl Into<String>,
796 app_id: impl Into<String>,
797 ) -> Self {
798 Self {
799 secret_id: secret_id.into(),
800 secret_key: secret_key.into(),
801 app_id: app_id.into(),
802 default_sign_name: None,
803 region: "ap-guangzhou".to_string(),
804 endpoint: "sms.tencentcloudapi.com".to_string(),
805 }
806 }
807
808 pub fn with_default_sign_name(mut self, sign_name: impl Into<String>) -> Self {
810 self.default_sign_name = Some(sign_name.into());
811 self
812 }
813
814 pub fn with_region(mut self, region: impl Into<String>) -> Self {
816 self.region = region.into();
817 self
818 }
819
820 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
822 self.endpoint = endpoint.into();
823 self
824 }
825}
826
827#[derive(Debug, Clone, serde::Serialize)]
838struct TencentSmsPayload {
839 #[serde(rename = "PhoneNumbers")]
841 phone_numbers: Vec<String>,
842 #[serde(rename = "TemplateId")]
844 template_id: String,
845 #[serde(rename = "TemplateParamSet")]
847 template_param_set: Vec<String>,
848 #[serde(rename = "SmsSdkAppId")]
850 sms_sdk_app_id: String,
851 #[serde(rename = "SignName", skip_serializing_if = "Option::is_none")]
853 sign_name: Option<String>,
854}
855
856pub struct TencentSmsNotifier {
887 config: TencentSmsConfig,
889 transport: Arc<dyn HttpTransport>,
891}
892
893impl TencentSmsNotifier {
894 pub fn new(config: TencentSmsConfig, transport: Arc<dyn HttpTransport>) -> Self {
901 Self { config, transport }
902 }
903
904 fn build_payload(&self, message: &SmsMessage) -> Result<String, NotifyError> {
919 let sign_name = message
920 .sign_name
921 .clone()
922 .or_else(|| self.config.default_sign_name.clone());
923
924 let payload = TencentSmsPayload {
925 phone_numbers: vec![message.phone.clone()],
926 template_id: message.template_id.clone(),
927 template_param_set: message.template_params.clone(),
928 sms_sdk_app_id: self.config.app_id.clone(),
929 sign_name,
930 };
931
932 serde_json::to_string(&payload).map_err(|e| NotifyError::Serialize(e.to_string()))
933 }
934}
935
936impl SmsNotifier for TencentSmsNotifier {
937 fn send_sms(&self, message: SmsMessage) -> Result<(), NotifyError> {
938 message.validate()?;
940
941 if self.config.secret_id.is_empty() {
943 return Err(NotifyError::MissingField("secret_id".into()));
944 }
945 if self.config.secret_key.is_empty() {
946 return Err(NotifyError::MissingField("secret_key".into()));
947 }
948 if self.config.app_id.is_empty() {
949 return Err(NotifyError::MissingField("app_id".into()));
950 }
951
952 let body = self.build_payload(&message)?;
954
955 let url = format!("https://{}/", self.config.endpoint);
957
958 self.transport
960 .post_json(&url, &body)
961 .map_err(|e| NotifyError::HttpTransport(format!("腾讯云短信发送失败: {e}")))?;
962
963 Ok(())
964 }
965}
966
967#[cfg(test)]
972mod tests {
973 use super::*;
974
975 #[test]
981 fn test_notify_level_default() {
982 let level = NotifyLevel::default();
983 assert_eq!(level, NotifyLevel::Info);
984 }
985
986 #[test]
988 fn test_notify_level_slack_color() {
989 assert_eq!(NotifyLevel::Info.slack_color(), "#36a64f");
990 assert_eq!(NotifyLevel::Warning.slack_color(), "#ffcc00");
991 assert_eq!(NotifyLevel::Error.slack_color(), "#ff0000");
992 assert_eq!(NotifyLevel::Critical.slack_color(), "#b22222");
993 }
994
995 #[test]
997 fn test_notify_level_as_str() {
998 assert_eq!(NotifyLevel::Info.as_str(), "info");
999 assert_eq!(NotifyLevel::Warning.as_str(), "warning");
1000 assert_eq!(NotifyLevel::Error.as_str(), "error");
1001 assert_eq!(NotifyLevel::Critical.as_str(), "critical");
1002 }
1003
1004 #[test]
1006 fn test_notify_level_display() {
1007 assert_eq!(format!("{}", NotifyLevel::Info), "info");
1008 assert_eq!(format!("{}", NotifyLevel::Warning), "warning");
1009 assert_eq!(format!("{}", NotifyLevel::Error), "error");
1010 assert_eq!(format!("{}", NotifyLevel::Critical), "critical");
1011 }
1012
1013 #[test]
1015 fn test_notify_level_from_str() {
1016 assert_eq!("info".parse::<NotifyLevel>().unwrap(), NotifyLevel::Info);
1018 assert_eq!(
1019 "warning".parse::<NotifyLevel>().unwrap(),
1020 NotifyLevel::Warning
1021 );
1022 assert_eq!("error".parse::<NotifyLevel>().unwrap(), NotifyLevel::Error);
1023 assert_eq!(
1024 "critical".parse::<NotifyLevel>().unwrap(),
1025 NotifyLevel::Critical
1026 );
1027
1028 assert_eq!("warn".parse::<NotifyLevel>().unwrap(), NotifyLevel::Warning);
1030 assert_eq!("err".parse::<NotifyLevel>().unwrap(), NotifyLevel::Error);
1031 assert_eq!(
1032 "crit".parse::<NotifyLevel>().unwrap(),
1033 NotifyLevel::Critical
1034 );
1035
1036 assert_eq!("INFO".parse::<NotifyLevel>().unwrap(), NotifyLevel::Info);
1038 assert_eq!(
1039 "Critical".parse::<NotifyLevel>().unwrap(),
1040 NotifyLevel::Critical
1041 );
1042
1043 assert!("unknown".parse::<NotifyLevel>().is_err());
1045 }
1046
1047 #[test]
1053 fn test_notification_builder() {
1054 let notification = Notification::new()
1055 .channel("slack")
1056 .title("部署完成")
1057 .content("服务已成功部署到生产环境")
1058 .level(NotifyLevel::Info)
1059 .metadata(serde_json::json!({"env": "prod"}));
1060
1061 assert_eq!(notification.channel, "slack");
1062 assert_eq!(notification.title, "部署完成");
1063 assert_eq!(notification.content, "服务已成功部署到生产环境");
1064 assert_eq!(notification.level, NotifyLevel::Info);
1065 assert_eq!(notification.metadata["env"], "prod");
1066 }
1067
1068 #[test]
1070 fn test_notification_default() {
1071 let notification = Notification::default();
1072 assert!(notification.channel.is_empty());
1073 assert!(notification.title.is_empty());
1074 assert!(notification.content.is_empty());
1075 assert_eq!(notification.level, NotifyLevel::Info);
1076 assert!(notification.metadata.is_null());
1077 }
1078
1079 #[test]
1081 fn test_notification_validate_ok() {
1082 let notification = Notification::new()
1083 .channel("slack")
1084 .title("标题")
1085 .content("内容");
1086 assert!(notification.validate().is_ok());
1087 }
1088
1089 #[test]
1091 fn test_notification_validate_missing_channel() {
1092 let notification = Notification::new().title("标题").content("内容");
1093 let err = notification.validate().unwrap_err();
1094 match err {
1095 NotifyError::MissingField(field) => assert_eq!(field, "channel"),
1096 other => panic!("期望 MissingField, 实际 {other:?}"),
1097 }
1098 }
1099
1100 #[test]
1102 fn test_notification_validate_missing_title() {
1103 let notification = Notification::new().channel("slack").content("内容");
1104 let err = notification.validate().unwrap_err();
1105 match err {
1106 NotifyError::MissingField(field) => assert_eq!(field, "title"),
1107 other => panic!("期望 MissingField, 实际 {other:?}"),
1108 }
1109 }
1110
1111 #[test]
1113 fn test_notification_validate_missing_content() {
1114 let notification = Notification::new().channel("slack").title("标题");
1115 let err = notification.validate().unwrap_err();
1116 match err {
1117 NotifyError::MissingField(field) => assert_eq!(field, "content"),
1118 other => panic!("期望 MissingField, 实际 {other:?}"),
1119 }
1120 }
1121
1122 #[test]
1128 fn test_memory_notifier_send() {
1129 let notifier = MemoryNotifier::new();
1130 let notification = Notification::new()
1131 .channel("slack")
1132 .title("标题")
1133 .content("内容")
1134 .level(NotifyLevel::Warning);
1135
1136 notifier.send(notification).unwrap();
1137 assert_eq!(notifier.count(), 1);
1138
1139 let last = notifier.last().unwrap();
1140 assert_eq!(last.channel, "slack");
1141 assert_eq!(last.title, "标题");
1142 assert_eq!(last.content, "内容");
1143 assert_eq!(last.level, NotifyLevel::Warning);
1144 }
1145
1146 #[test]
1148 fn test_memory_notifier_send_multiple() {
1149 let notifier = MemoryNotifier::new();
1150 for i in 0..5 {
1151 notifier
1152 .send(
1153 Notification::new()
1154 .channel("slack")
1155 .title(format!("标题{i}"))
1156 .content("内容"),
1157 )
1158 .unwrap();
1159 }
1160 assert_eq!(notifier.count(), 5);
1161
1162 let all = notifier.all();
1163 assert_eq!(all[0].title, "标题0");
1164 assert_eq!(all[4].title, "标题4");
1165 }
1166
1167 #[test]
1169 fn test_memory_notifier_send_invalid() {
1170 let notifier = MemoryNotifier::new();
1171 let notification = Notification::new().title("标题").content("内容");
1172 assert!(notifier.send(notification).is_err());
1174 assert_eq!(notifier.count(), 0);
1175 }
1176
1177 #[test]
1179 fn test_memory_notifier_clear() {
1180 let notifier = MemoryNotifier::new();
1181 notifier
1182 .send(
1183 Notification::new()
1184 .channel("slack")
1185 .title("标题")
1186 .content("内容"),
1187 )
1188 .unwrap();
1189 assert_eq!(notifier.count(), 1);
1190
1191 notifier.clear();
1192 assert_eq!(notifier.count(), 0);
1193 assert!(notifier.last().is_none());
1194 }
1195
1196 #[test]
1202 fn test_memory_http_transport_post_json() {
1203 let transport = MemoryHttpTransport::new();
1204 transport
1205 .post_json("https://hooks.slack.com/services/xxx", r#"{"text":"hi"}"#)
1206 .unwrap();
1207
1208 assert_eq!(transport.count(), 1);
1209 let (url, body) = transport.last().unwrap();
1210 assert_eq!(url, "https://hooks.slack.com/services/xxx");
1211 assert_eq!(body, r#"{"text":"hi"}"#);
1212 }
1213
1214 #[test]
1216 fn test_memory_http_transport_clear() {
1217 let transport = MemoryHttpTransport::new();
1218 transport.post_json("url", "body").unwrap();
1219 assert_eq!(transport.count(), 1);
1220
1221 transport.clear();
1222 assert_eq!(transport.count(), 0);
1223 }
1224
1225 #[test]
1231 fn test_slack_config_builder() {
1232 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X")
1233 .with_channel("#alerts")
1234 .with_username("SZ-Rust Bot")
1235 .with_icon_emoji(":alarm_clock:");
1236
1237 assert_eq!(config.webhook_url, "https://hooks.slack.com/services/T/B/X");
1238 assert_eq!(config.channel.as_deref(), Some("#alerts"));
1239 assert_eq!(config.username.as_deref(), Some("SZ-Rust Bot"));
1240 assert_eq!(config.icon_emoji.as_deref(), Some(":alarm_clock:"));
1241 }
1242
1243 #[test]
1245 fn test_slack_config_minimal() {
1246 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X");
1247 assert_eq!(config.webhook_url, "https://hooks.slack.com/services/T/B/X");
1248 assert!(config.channel.is_none());
1249 assert!(config.username.is_none());
1250 assert!(config.icon_emoji.is_none());
1251 }
1252
1253 #[test]
1259 fn test_slack_notifier_send() {
1260 let transport = Arc::new(MemoryHttpTransport::new());
1261 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X")
1262 .with_channel("#alerts")
1263 .with_username("SZ-Rust Bot")
1264 .with_icon_emoji(":alarm_clock:");
1265 let notifier = SlackNotifier::new(config, transport.clone());
1266
1267 let notification = Notification::new()
1268 .channel("slack")
1269 .title("部署完成")
1270 .content("服务已成功部署到生产环境")
1271 .level(NotifyLevel::Info);
1272
1273 notifier.send(notification).unwrap();
1274
1275 assert_eq!(transport.count(), 1);
1277 let (url, body) = transport.last().unwrap();
1278 assert_eq!(url, "https://hooks.slack.com/services/T/B/X");
1279
1280 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1282 assert!(payload["text"].as_str().unwrap().contains("部署完成"));
1283 assert!(payload["text"]
1284 .as_str()
1285 .unwrap()
1286 .contains("服务已成功部署到生产环境"));
1287 assert!(payload["text"].as_str().unwrap().contains("[INFO]"));
1288 assert_eq!(payload["channel"], "#alerts");
1289 assert_eq!(payload["username"], "SZ-Rust Bot");
1290 assert_eq!(payload["icon_emoji"], ":alarm_clock:");
1291
1292 let attachments = payload["attachments"].as_array().unwrap();
1294 assert_eq!(attachments.len(), 1);
1295 assert_eq!(attachments[0]["color"], "#36a64f"); assert_eq!(attachments[0]["title"], "部署完成");
1297 assert_eq!(attachments[0]["text"], "服务已成功部署到生产环境");
1298 assert!(attachments[0]["ts"].as_i64().is_some());
1299 }
1300
1301 #[test]
1303 fn test_slack_notifier_level_colors() {
1304 let transport = Arc::new(MemoryHttpTransport::new());
1305 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X");
1306 let notifier = SlackNotifier::new(config, transport.clone());
1307
1308 notifier
1310 .send(
1311 Notification::new()
1312 .channel("slack")
1313 .title("w")
1314 .content("c")
1315 .level(NotifyLevel::Warning),
1316 )
1317 .unwrap();
1318 let (_, body) = transport.last().unwrap();
1319 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1320 assert_eq!(payload["attachments"][0]["color"], "#ffcc00");
1321
1322 notifier
1324 .send(
1325 Notification::new()
1326 .channel("slack")
1327 .title("w")
1328 .content("c")
1329 .level(NotifyLevel::Error),
1330 )
1331 .unwrap();
1332 let (_, body) = transport.last().unwrap();
1333 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1334 assert_eq!(payload["attachments"][0]["color"], "#ff0000");
1335
1336 notifier
1338 .send(
1339 Notification::new()
1340 .channel("slack")
1341 .title("w")
1342 .content("c")
1343 .level(NotifyLevel::Critical),
1344 )
1345 .unwrap();
1346 let (_, body) = transport.last().unwrap();
1347 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1348 assert_eq!(payload["attachments"][0]["color"], "#b22222");
1349
1350 assert_eq!(transport.count(), 3);
1351 }
1352
1353 #[test]
1355 fn test_slack_notifier_missing_channel() {
1356 let transport = Arc::new(MemoryHttpTransport::new());
1357 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X");
1358 let notifier = SlackNotifier::new(config, transport.clone());
1359
1360 let notification = Notification::new().title("标题").content("内容");
1361 let err = notifier.send(notification).unwrap_err();
1362 match err {
1363 NotifyError::MissingField(field) => assert_eq!(field, "channel"),
1364 other => panic!("期望 MissingField, 实际 {other:?}"),
1365 }
1366
1367 assert_eq!(transport.count(), 0);
1369 }
1370
1371 #[test]
1373 fn test_slack_notifier_missing_webhook_url() {
1374 let transport = Arc::new(MemoryHttpTransport::new());
1375 let config = SlackConfig::new(""); let notifier = SlackNotifier::new(config, transport.clone());
1377
1378 let notification = Notification::new()
1379 .channel("slack")
1380 .title("标题")
1381 .content("内容");
1382 let err = notifier.send(notification).unwrap_err();
1383 match err {
1384 NotifyError::MissingField(field) => assert_eq!(field, "webhook_url"),
1385 other => panic!("期望 MissingField, 实际 {other:?}"),
1386 }
1387
1388 assert_eq!(transport.count(), 0);
1389 }
1390
1391 #[test]
1393 fn test_slack_notifier_http_failure() {
1394 struct FailingTransport;
1396 impl HttpTransport for FailingTransport {
1397 fn post_json(&self, _url: &str, _body: &str) -> Result<(), NotifyError> {
1398 Err(NotifyError::HttpTransport("connection refused".to_string()))
1399 }
1400 }
1401
1402 let transport = Arc::new(FailingTransport);
1403 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X");
1404 let notifier = SlackNotifier::new(config, transport);
1405
1406 let notification = Notification::new()
1407 .channel("slack")
1408 .title("标题")
1409 .content("内容");
1410 let err = notifier.send(notification).unwrap_err();
1411 match err {
1412 NotifyError::HttpTransport(msg) => assert!(msg.contains("connection refused")),
1413 other => panic!("期望 HttpTransport, 实际 {other:?}"),
1414 }
1415 }
1416
1417 #[test]
1419 fn test_slack_notifier_build_payload() {
1420 let transport: Arc<dyn HttpTransport> = Arc::new(MemoryHttpTransport::new());
1421 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X")
1422 .with_channel("#alerts")
1423 .with_username("Bot")
1424 .with_icon_emoji(":bell:");
1425 let notifier = SlackNotifier::new(config, transport.clone());
1426
1427 let notification = Notification::new()
1428 .channel("slack")
1429 .title("Test Title")
1430 .content("Test Content")
1431 .level(NotifyLevel::Error);
1432
1433 let payload_json = notifier.build_payload(¬ification).unwrap();
1434 let payload: serde_json::Value = serde_json::from_str(&payload_json).unwrap();
1435
1436 assert_eq!(
1438 payload["text"].as_str().unwrap(),
1439 "[ERROR] Test Title — Test Content"
1440 );
1441
1442 assert_eq!(payload["channel"], "#alerts");
1444 assert_eq!(payload["username"], "Bot");
1445 assert_eq!(payload["icon_emoji"], ":bell:");
1446
1447 let attachments = payload["attachments"].as_array().unwrap();
1449 assert_eq!(attachments.len(), 1);
1450 assert_eq!(attachments[0]["color"], "#ff0000");
1451 assert_eq!(attachments[0]["title"], "Test Title");
1452 assert_eq!(attachments[0]["text"], "Test Content");
1453 assert!(attachments[0]["ts"].as_i64().is_some());
1454 }
1455
1456 #[test]
1458 fn test_slack_notifier_send_multiple() {
1459 let transport = Arc::new(MemoryHttpTransport::new());
1460 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X");
1461 let notifier = SlackNotifier::new(config, transport.clone());
1462
1463 for i in 0..3 {
1464 notifier
1465 .send(
1466 Notification::new()
1467 .channel("slack")
1468 .title(format!("Title {i}"))
1469 .content("content"),
1470 )
1471 .unwrap();
1472 }
1473 assert_eq!(transport.count(), 3);
1474 }
1475
1476 #[test]
1478 fn test_slack_notifier_minimal_config() {
1479 let transport = Arc::new(MemoryHttpTransport::new());
1480 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X");
1481 let notifier = SlackNotifier::new(config, transport.clone());
1482
1483 notifier
1484 .send(
1485 Notification::new()
1486 .channel("slack")
1487 .title("Title")
1488 .content("Content"),
1489 )
1490 .unwrap();
1491
1492 let (_, body) = transport.last().unwrap();
1493 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1494
1495 assert!(payload.get("channel").is_none());
1497 assert!(payload.get("username").is_none());
1498 assert!(payload.get("icon_emoji").is_none());
1499 }
1500
1501 #[test]
1503 fn test_slack_notifier_with_metadata() {
1504 let transport = Arc::new(MemoryHttpTransport::new());
1505 let config = SlackConfig::new("https://hooks.slack.com/services/T/B/X");
1506 let notifier = SlackNotifier::new(config, transport.clone());
1507
1508 notifier
1509 .send(
1510 Notification::new()
1511 .channel("slack")
1512 .title("Title")
1513 .content("Content")
1514 .metadata(serde_json::json!({"env": "prod", "version": "1.0.0"})),
1515 )
1516 .unwrap();
1517
1518 assert_eq!(transport.count(), 1);
1519 }
1520
1521 #[test]
1527 fn test_sms_message_builder() {
1528 let msg = SmsMessage::new()
1529 .phone("+8613800138000")
1530 .template_id("123456")
1531 .template_param("1234")
1532 .template_param("5")
1533 .sign_name("鲜视达科技")
1534 .metadata(serde_json::json!({"scene": "login"}));
1535
1536 assert_eq!(msg.phone, "+8613800138000");
1537 assert_eq!(msg.template_id, "123456");
1538 assert_eq!(msg.template_params, vec!["1234", "5"]);
1539 assert_eq!(msg.sign_name.as_deref(), Some("鲜视达科技"));
1540 assert_eq!(msg.metadata["scene"], "login");
1541 }
1542
1543 #[test]
1545 fn test_sms_message_validate_ok() {
1546 let msg = SmsMessage::new()
1547 .phone("+8613800138000")
1548 .template_id("123456");
1549 assert!(msg.validate().is_ok());
1550 }
1551
1552 #[test]
1554 fn test_sms_message_validate_missing_phone() {
1555 let msg = SmsMessage::new().template_id("123456");
1556 let err = msg.validate().unwrap_err();
1557 match err {
1558 NotifyError::MissingField(field) => assert_eq!(field, "phone"),
1559 other => panic!("期望 MissingField, 实际 {other:?}"),
1560 }
1561 }
1562
1563 #[test]
1565 fn test_sms_message_validate_missing_template_id() {
1566 let msg = SmsMessage::new().phone("+8613800138000");
1567 let err = msg.validate().unwrap_err();
1568 match err {
1569 NotifyError::MissingField(field) => assert_eq!(field, "template_id"),
1570 other => panic!("期望 MissingField, 实际 {other:?}"),
1571 }
1572 }
1573
1574 #[test]
1580 fn test_memory_sms_notifier_send() {
1581 let notifier = MemorySmsNotifier::new();
1582 let msg = SmsMessage::new()
1583 .phone("+8613800138000")
1584 .template_id("123456")
1585 .template_param("1234");
1586
1587 notifier.send_sms(msg).unwrap();
1588 assert_eq!(notifier.count(), 1);
1589
1590 let last = notifier.last().unwrap();
1591 assert_eq!(last.phone, "+8613800138000");
1592 assert_eq!(last.template_id, "123456");
1593 assert_eq!(last.template_params, vec!["1234"]);
1594 }
1595
1596 #[test]
1598 fn test_memory_sms_notifier_send_multiple() {
1599 let notifier = MemorySmsNotifier::new();
1600 for i in 0..5 {
1601 notifier
1602 .send_sms(
1603 SmsMessage::new()
1604 .phone(format!("+861380013{i:04}"))
1605 .template_id("123456"),
1606 )
1607 .unwrap();
1608 }
1609 assert_eq!(notifier.count(), 5);
1610
1611 let all = notifier.all();
1612 assert_eq!(all[0].phone, "+8613800130000");
1613 assert_eq!(all[4].phone, "+8613800130004");
1614 }
1615
1616 #[test]
1618 fn test_memory_sms_notifier_send_invalid() {
1619 let notifier = MemorySmsNotifier::new();
1620 let msg = SmsMessage::new().template_id("123456");
1621 assert!(notifier.send_sms(msg).is_err());
1623 assert_eq!(notifier.count(), 0);
1624 }
1625
1626 #[test]
1628 fn test_memory_sms_notifier_clear() {
1629 let notifier = MemorySmsNotifier::new();
1630 notifier
1631 .send_sms(
1632 SmsMessage::new()
1633 .phone("+8613800138000")
1634 .template_id("123456"),
1635 )
1636 .unwrap();
1637 assert_eq!(notifier.count(), 1);
1638
1639 notifier.clear();
1640 assert_eq!(notifier.count(), 0);
1641 assert!(notifier.last().is_none());
1642 }
1643
1644 #[test]
1650 fn test_tencent_sms_config_builder() {
1651 let config = TencentSmsConfig::new("AKIDxxx", "SKxxx", "1400000000")
1652 .with_default_sign_name("鲜视达科技")
1653 .with_region("ap-beijing")
1654 .with_endpoint("sms.tencentcloudapi.com");
1655
1656 assert_eq!(config.secret_id, "AKIDxxx");
1657 assert_eq!(config.secret_key, "SKxxx");
1658 assert_eq!(config.app_id, "1400000000");
1659 assert_eq!(config.default_sign_name.as_deref(), Some("鲜视达科技"));
1660 assert_eq!(config.region, "ap-beijing");
1661 assert_eq!(config.endpoint, "sms.tencentcloudapi.com");
1662 }
1663
1664 #[test]
1666 fn test_tencent_sms_config_minimal() {
1667 let config = TencentSmsConfig::new("AKIDxxx", "SKxxx", "1400000000");
1668 assert_eq!(config.secret_id, "AKIDxxx");
1669 assert_eq!(config.secret_key, "SKxxx");
1670 assert_eq!(config.app_id, "1400000000");
1671 assert!(config.default_sign_name.is_none());
1672 assert_eq!(config.region, "ap-guangzhou");
1673 assert_eq!(config.endpoint, "sms.tencentcloudapi.com");
1674 }
1675
1676 #[test]
1682 fn test_tencent_sms_notifier_send() {
1683 let transport = Arc::new(MemoryHttpTransport::new());
1684 let config = TencentSmsConfig::new("AKIDxxx", "SKxxx", "1400000000")
1685 .with_default_sign_name("鲜视达科技");
1686 let notifier = TencentSmsNotifier::new(config, transport.clone());
1687
1688 let msg = SmsMessage::new()
1689 .phone("+8613800138000")
1690 .template_id("123456")
1691 .template_param("1234")
1692 .template_param("5");
1693
1694 notifier.send_sms(msg).unwrap();
1695
1696 assert_eq!(transport.count(), 1);
1698 let (url, body) = transport.last().unwrap();
1699 assert_eq!(url, "https://sms.tencentcloudapi.com/");
1700
1701 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1703 assert_eq!(payload["PhoneNumbers"][0], "+8613800138000");
1704 assert_eq!(payload["TemplateId"], "123456");
1705 assert_eq!(payload["TemplateParamSet"][0], "1234");
1706 assert_eq!(payload["TemplateParamSet"][1], "5");
1707 assert_eq!(payload["SmsSdkAppId"], "1400000000");
1708 assert_eq!(payload["SignName"], "鲜视达科技");
1709 }
1710
1711 #[test]
1713 fn test_tencent_sms_notifier_missing_phone() {
1714 let transport = Arc::new(MemoryHttpTransport::new());
1715 let config = TencentSmsConfig::new("AKIDxxx", "SKxxx", "1400000000");
1716 let notifier = TencentSmsNotifier::new(config, transport.clone());
1717
1718 let msg = SmsMessage::new().template_id("123456");
1719 let err = notifier.send_sms(msg).unwrap_err();
1720 match err {
1721 NotifyError::MissingField(field) => assert_eq!(field, "phone"),
1722 other => panic!("期望 MissingField, 实际 {other:?}"),
1723 }
1724
1725 assert_eq!(transport.count(), 0);
1727 }
1728
1729 #[test]
1731 fn test_tencent_sms_notifier_missing_credentials() {
1732 let transport = Arc::new(MemoryHttpTransport::new());
1733 let config = TencentSmsConfig::new("", "SKxxx", "1400000000");
1735 let notifier = TencentSmsNotifier::new(config, transport.clone());
1736
1737 let msg = SmsMessage::new()
1738 .phone("+8613800138000")
1739 .template_id("123456");
1740 let err = notifier.send_sms(msg).unwrap_err();
1741 match err {
1742 NotifyError::MissingField(field) => assert_eq!(field, "secret_id"),
1743 other => panic!("期望 MissingField, 实际 {other:?}"),
1744 }
1745
1746 assert_eq!(transport.count(), 0);
1748
1749 let config2 = TencentSmsConfig::new("AKIDxxx", "", "1400000000");
1751 let notifier2 = TencentSmsNotifier::new(config2, transport.clone());
1752 let msg2 = SmsMessage::new()
1753 .phone("+8613800138000")
1754 .template_id("123456");
1755 let err2 = notifier2.send_sms(msg2).unwrap_err();
1756 match err2 {
1757 NotifyError::MissingField(field) => assert_eq!(field, "secret_key"),
1758 other => panic!("期望 MissingField, 实际 {other:?}"),
1759 }
1760
1761 let config3 = TencentSmsConfig::new("AKIDxxx", "SKxxx", "");
1763 let notifier3 = TencentSmsNotifier::new(config3, transport.clone());
1764 let msg3 = SmsMessage::new()
1765 .phone("+8613800138000")
1766 .template_id("123456");
1767 let err3 = notifier3.send_sms(msg3).unwrap_err();
1768 match err3 {
1769 NotifyError::MissingField(field) => assert_eq!(field, "app_id"),
1770 other => panic!("期望 MissingField, 实际 {other:?}"),
1771 }
1772
1773 assert_eq!(transport.count(), 0);
1775 }
1776
1777 #[test]
1779 fn test_tencent_sms_notifier_uses_default_sign_name() {
1780 let transport = Arc::new(MemoryHttpTransport::new());
1781 let config = TencentSmsConfig::new("AKIDxxx", "SKxxx", "1400000000")
1782 .with_default_sign_name("鲜视达科技");
1783 let notifier = TencentSmsNotifier::new(config, transport.clone());
1784
1785 let msg = SmsMessage::new()
1787 .phone("+8613800138000")
1788 .template_id("123456");
1789
1790 notifier.send_sms(msg).unwrap();
1791
1792 let (_, body) = transport.last().unwrap();
1793 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1794 assert_eq!(payload["SignName"], "鲜视达科技");
1795
1796 let msg2 = SmsMessage::new()
1798 .phone("+8613800138000")
1799 .template_id("123456")
1800 .sign_name("覆盖签名");
1801 notifier.send_sms(msg2).unwrap();
1802
1803 let (_, body2) = transport.last().unwrap();
1804 let payload2: serde_json::Value = serde_json::from_str(&body2).unwrap();
1805 assert_eq!(payload2["SignName"], "覆盖签名");
1806
1807 assert_eq!(transport.count(), 2);
1808 }
1809
1810 #[test]
1812 fn test_tencent_sms_notifier_no_sign_name() {
1813 let transport = Arc::new(MemoryHttpTransport::new());
1814 let config = TencentSmsConfig::new("AKIDxxx", "SKxxx", "1400000000");
1815 let notifier = TencentSmsNotifier::new(config, transport.clone());
1816
1817 let msg = SmsMessage::new()
1818 .phone("+8613800138000")
1819 .template_id("123456");
1820
1821 notifier.send_sms(msg).unwrap();
1822
1823 let (_, body) = transport.last().unwrap();
1824 let payload: serde_json::Value = serde_json::from_str(&body).unwrap();
1825 assert!(payload.get("SignName").is_none());
1827 }
1828}