1use crate::error::{OptimError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::{Duration, SystemTime};
10
11use super::config::{
12 CustomIntegrationConfig, EmailIntegration, EmailTemplateConfig, GitHubIntegration,
13 GitHubLabelConfig, GitHubStatusCheckConfig, HttpMethod, IntegrationConfig, PayloadFormat,
14 SlackIntegration, SlackNotificationConfig, SmtpConfig, WebhookAuth, WebhookIntegration,
15 WebhookPayloadConfig, WebhookRetryConfig, WebhookTriggerConfig,
16};
17use super::reporting::GeneratedReport;
18use super::test_execution::{CiCdTestResult, TestExecutionStatus, TestSuiteStatistics};
19
20#[derive(Debug)]
22pub struct IntegrationManager {
23 pub config: IntegrationConfig,
25 pub github_client: Option<GitHubClient>,
27 pub slack_client: Option<SlackClient>,
29 pub email_client: Option<EmailClient>,
31 pub webhook_clients: Vec<WebhookClient>,
33 pub custom_integrations: HashMap<String, Box<dyn CustomIntegration>>,
35 pub statistics: IntegrationStatistics,
37}
38
39#[derive(Debug, Clone)]
41pub struct GitHubClient {
42 pub config: GitHubIntegration,
44 pub http_client: HttpClient,
46 pub rate_limiter: RateLimiter,
48}
49
50#[derive(Debug, Clone)]
52pub struct SlackClient {
53 pub config: SlackIntegration,
55 pub http_client: HttpClient,
57}
58
59#[derive(Debug, Clone)]
61pub struct EmailClient {
62 pub config: EmailIntegration,
64 pub smtp_client: SmtpClient,
66}
67
68#[derive(Debug, Clone)]
70pub struct WebhookClient {
71 pub config: WebhookIntegration,
73 pub http_client: HttpClient,
75 pub retry_manager: RetryManager,
77}
78
79pub trait CustomIntegration: std::fmt::Debug + Send + Sync {
81 fn initialize(&mut self, config: &HashMap<String, String>) -> Result<()>;
83
84 fn send_notification(&self, notification: &IntegrationNotification) -> Result<()>;
86
87 fn handle_test_results(
89 &self,
90 results: &[CiCdTestResult],
91 statistics: &TestSuiteStatistics,
92 ) -> Result<()>;
93
94 fn handle_report_generated(&self, report: &GeneratedReport) -> Result<()>;
96
97 fn get_status(&self) -> IntegrationStatus;
99
100 fn validate_config(&self, config: &HashMap<String, String>) -> Result<()>;
102}
103
104#[derive(Debug, Clone)]
106pub struct HttpClient {
107 pub base_url: String,
109 pub default_headers: HashMap<String, String>,
111 pub timeout: Duration,
113 pub user_agent: String,
115}
116
117#[derive(Debug, Clone)]
119pub struct SmtpClient {
120 pub config: SmtpConfig,
122 pub connection_pool: SmtpConnectionPool,
124}
125
126#[derive(Debug, Clone)]
128pub struct SmtpConnectionPool {
129 pub max_connections: usize,
131 pub active_connections: usize,
133 pub connection_timeout: Duration,
135}
136
137#[derive(Debug, Clone)]
139pub struct RateLimiter {
140 pub requests_per_minute: u32,
142 pub current_requests: u32,
144 pub reset_time: SystemTime,
146 pub request_history: Vec<SystemTime>,
148}
149
150#[derive(Debug, Clone)]
152pub struct RetryManager {
153 pub config: WebhookRetryConfig,
155 pub failed_requests: Vec<FailedRequest>,
157 pub statistics: RetryStatistics,
159}
160
161#[derive(Debug, Clone)]
163pub struct FailedRequest {
164 pub id: String,
166 pub request_data: RequestData,
168 pub failed_at: SystemTime,
170 pub retry_attempts: u32,
172 pub last_error: String,
174 pub next_retry_at: SystemTime,
176}
177
178#[derive(Debug, Clone)]
180pub struct RequestData {
181 pub method: HttpMethod,
183 pub url: String,
185 pub headers: HashMap<String, String>,
187 pub body: String,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct RetryStatistics {
194 pub total_retries: u64,
196 pub successful_retries: u64,
198 pub failed_retries: u64,
200 pub average_retry_delay_sec: f64,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct IntegrationNotification {
207 pub notification_type: NotificationType,
209 pub title: String,
211 pub message: String,
213 pub priority: NotificationPriority,
215 pub data: HashMap<String, String>,
217 pub timestamp: SystemTime,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223pub enum NotificationType {
224 TestCompletion,
226 TestFailure,
228 PerformanceRegression,
230 PerformanceImprovement,
232 ReportGenerated,
234 SystemAlert,
236 Custom(String),
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
242pub enum NotificationPriority {
243 Low,
245 Normal,
247 High,
249 Critical,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
255pub enum IntegrationStatus {
256 Healthy,
258 Warning,
260 Error,
262 Disabled,
264 NotConfigured,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, Default)]
270pub struct IntegrationStatistics {
271 pub github: Option<GitHubStatistics>,
273 pub slack: Option<SlackStatistics>,
275 pub email: Option<EmailStatistics>,
277 pub webhook: WebhookStatistics,
279 pub custom: HashMap<String, CustomIntegrationStatistics>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct GitHubStatistics {
286 pub status_checks_created: u64,
288 pub pr_comments_created: u64,
290 pub issues_created: u64,
292 pub api_requests: u64,
294 pub rate_limit_hits: u64,
296 pub last_activity: Option<SystemTime>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct SlackStatistics {
303 pub messages_sent: u64,
305 pub files_uploaded: u64,
307 pub channels_used: Vec<String>,
309 pub last_activity: Option<SystemTime>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct EmailStatistics {
316 pub emails_sent: u64,
318 pub emails_failed: u64,
320 pub recipients_contacted: Vec<String>,
322 pub last_activity: Option<SystemTime>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct WebhookStatistics {
329 pub requests_sent: u64,
331 pub requests_successful: u64,
333 pub requests_failed: u64,
335 pub average_response_time_ms: f64,
337 pub retry_stats: RetryStatistics,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct CustomIntegrationStatistics {
344 pub name: String,
346 pub operations_performed: u64,
348 pub operations_successful: u64,
350 pub operations_failed: u64,
352 pub last_activity: Option<SystemTime>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct GitHubStatusCheckResponse {
359 pub id: u64,
361 pub state: String,
363 pub description: String,
365 pub target_url: Option<String>,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct GitHubCommentResponse {
372 pub id: u64,
374 pub body: String,
376 pub html_url: String,
378 pub created_at: String,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct GitHubIssueResponse {
385 pub id: u64,
387 pub number: u64,
389 pub title: String,
391 pub html_url: String,
393 pub state: String,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct SlackMessageResponse {
400 pub ok: bool,
402 pub ts: Option<String>,
404 pub channel: Option<String>,
406 pub error: Option<String>,
408}
409
410impl IntegrationManager {
411 pub fn new(config: IntegrationConfig) -> Result<Self> {
413 let mut manager = Self {
414 config: config.clone(),
415 github_client: None,
416 slack_client: None,
417 email_client: None,
418 webhook_clients: Vec::new(),
419 custom_integrations: HashMap::new(),
420 statistics: IntegrationStatistics::default(),
421 };
422
423 manager.initialize_integrations()?;
424 Ok(manager)
425 }
426
427 fn initialize_integrations(&mut self) -> Result<()> {
429 if let Some(github_config) = &self.config.github {
431 self.github_client = Some(GitHubClient::new(github_config.clone())?);
432 }
433
434 if let Some(slack_config) = &self.config.slack {
436 self.slack_client = Some(SlackClient::new(slack_config.clone())?);
437 }
438
439 if let Some(email_config) = &self.config.email {
441 self.email_client = Some(EmailClient::new(email_config.clone())?);
442 }
443
444 for webhook_config in &self.config.webhooks {
446 let client = WebhookClient::new(webhook_config.clone())?;
447 self.webhook_clients.push(client);
448 }
449
450 for (name, custom_config) in &self.config.custom {
452 if custom_config.enabled {
455 }
457 }
458
459 Ok(())
460 }
461
462 pub fn send_notification(&mut self, notification: &IntegrationNotification) -> Result<()> {
464 if self.should_send_to_github(¬ification.notification_type) {
466 if let Some(github_client) = &mut self.github_client {
467 github_client.send_notification(notification)?;
468 }
469 }
470
471 if self.should_send_to_slack(¬ification.notification_type) {
473 if let Some(slack_client) = &mut self.slack_client {
474 slack_client.send_notification(notification)?;
475 }
476 }
477
478 if self.should_send_to_email(¬ification.notification_type) {
480 if let Some(email_client) = &mut self.email_client {
481 email_client.send_notification(notification)?;
482 }
483 }
484
485 for webhook_client in &mut self.webhook_clients {
487 if webhook_client.should_trigger(¬ification.notification_type) {
488 webhook_client.send_notification(notification)?;
489 }
490 }
491
492 for (name, integration) in &self.custom_integrations {
494 integration.send_notification(notification)?;
495 }
496
497 Ok(())
498 }
499
500 pub fn handle_test_completion(
502 &mut self,
503 results: &[CiCdTestResult],
504 statistics: &TestSuiteStatistics,
505 ) -> Result<()> {
506 let notification_type = if statistics.failed > 0 {
507 NotificationType::TestFailure
508 } else {
509 NotificationType::TestCompletion
510 };
511
512 let priority = if statistics.failed > 0 {
513 NotificationPriority::High
514 } else {
515 NotificationPriority::Normal
516 };
517
518 let notification = IntegrationNotification {
519 notification_type,
520 title: format!(
521 "Test Suite Completed: {}/{} Passed",
522 statistics.passed, statistics.total_tests
523 ),
524 message: self.create_test_summary_message(results, statistics),
525 priority,
526 data: self.create_test_data_map(statistics),
527 timestamp: SystemTime::now(),
528 };
529
530 self.send_notification(¬ification)?;
531
532 for integration in self.custom_integrations.values() {
534 integration.handle_test_results(results, statistics)?;
535 }
536
537 Ok(())
538 }
539
540 pub fn handle_report_generated(&mut self, report: &GeneratedReport) -> Result<()> {
542 let notification = IntegrationNotification {
543 notification_type: NotificationType::ReportGenerated,
544 title: format!("Performance Report Generated: {:?}", report.report_type),
545 message: format!("Report available at: {:?}", report.file_path),
546 priority: NotificationPriority::Normal,
547 data: HashMap::new(),
548 timestamp: SystemTime::now(),
549 };
550
551 self.send_notification(¬ification)?;
552
553 for integration in self.custom_integrations.values() {
555 integration.handle_report_generated(report)?;
556 }
557
558 Ok(())
559 }
560
561 pub fn get_health_status(&self) -> IntegrationStatus {
563 let mut has_errors = false;
564 let mut has_warnings = false;
565
566 if let Some(github_client) = &self.github_client {
568 match github_client.get_health_status() {
569 IntegrationStatus::Error => has_errors = true,
570 IntegrationStatus::Warning => has_warnings = true,
571 _ => {}
572 }
573 }
574
575 if let Some(slack_client) = &self.slack_client {
577 match slack_client.get_health_status() {
578 IntegrationStatus::Error => has_errors = true,
579 IntegrationStatus::Warning => has_warnings = true,
580 _ => {}
581 }
582 }
583
584 if let Some(email_client) = &self.email_client {
586 match email_client.get_health_status() {
587 IntegrationStatus::Error => has_errors = true,
588 IntegrationStatus::Warning => has_warnings = true,
589 _ => {}
590 }
591 }
592
593 for webhook_client in &self.webhook_clients {
595 match webhook_client.get_health_status() {
596 IntegrationStatus::Error => has_errors = true,
597 IntegrationStatus::Warning => has_warnings = true,
598 _ => {}
599 }
600 }
601
602 for integration in self.custom_integrations.values() {
604 match integration.get_status() {
605 IntegrationStatus::Error => has_errors = true,
606 IntegrationStatus::Warning => has_warnings = true,
607 _ => {}
608 }
609 }
610
611 if has_errors {
612 IntegrationStatus::Error
613 } else if has_warnings {
614 IntegrationStatus::Warning
615 } else {
616 IntegrationStatus::Healthy
617 }
618 }
619
620 fn should_send_to_github(&self, notification_type: &NotificationType) -> bool {
622 if let Some(github_config) = &self.config.github {
623 match notification_type {
624 NotificationType::TestCompletion | NotificationType::TestFailure => {
625 github_config.create_status_checks || github_config.create_pr_comments
626 }
627 NotificationType::PerformanceRegression => github_config.create_regression_issues,
628 _ => false,
629 }
630 } else {
631 false
632 }
633 }
634
635 fn should_send_to_slack(&self, notification_type: &NotificationType) -> bool {
637 if let Some(slack_config) = &self.config.slack {
638 match notification_type {
639 NotificationType::TestCompletion => slack_config.notifications.notify_on_completion,
640 NotificationType::TestFailure => slack_config.notifications.notify_on_failure,
641 NotificationType::PerformanceRegression => {
642 slack_config.notifications.notify_on_regression
643 }
644 NotificationType::PerformanceImprovement => {
645 slack_config.notifications.notify_on_improvement
646 }
647 _ => true, }
649 } else {
650 false
651 }
652 }
653
654 fn should_send_to_email(&self, notification_type: &NotificationType) -> bool {
656 matches!(
658 notification_type,
659 NotificationType::TestFailure
660 | NotificationType::PerformanceRegression
661 | NotificationType::SystemAlert
662 )
663 }
664
665 fn create_test_summary_message(
667 &self,
668 results: &[CiCdTestResult],
669 statistics: &TestSuiteStatistics,
670 ) -> String {
671 let mut message = format!(
672 "Test Suite Results:\n• Total: {}\n• Passed: {}\n• Failed: {}\n• Skipped: {}\n• Success Rate: {:.1}%\n",
673 statistics.total_tests,
674 statistics.passed,
675 statistics.failed,
676 statistics.skipped,
677 statistics.success_rate * 100.0
678 );
679
680 if statistics.failed > 0 {
681 message.push_str("\nFailed Tests:\n");
682 for result in results
683 .iter()
684 .filter(|r| r.status == TestExecutionStatus::Failed)
685 .take(5)
686 {
687 message.push_str(&format!("• {}\n", result.test_name));
688 }
689 if statistics.failed > 5 {
690 message.push_str(&format!("• ... and {} more\n", statistics.failed - 5));
691 }
692 }
693
694 message
695 }
696
697 fn create_test_data_map(&self, statistics: &TestSuiteStatistics) -> HashMap<String, String> {
699 let mut data = HashMap::new();
700 data.insert(
701 "total_tests".to_string(),
702 statistics.total_tests.to_string(),
703 );
704 data.insert("passed_tests".to_string(), statistics.passed.to_string());
705 data.insert("failed_tests".to_string(), statistics.failed.to_string());
706 data.insert("skipped_tests".to_string(), statistics.skipped.to_string());
707 data.insert(
708 "success_rate".to_string(),
709 format!("{:.1}", statistics.success_rate * 100.0),
710 );
711 data.insert(
712 "duration_seconds".to_string(),
713 statistics.total_duration.as_secs().to_string(),
714 );
715 data
716 }
717}
718
719impl GitHubClient {
720 pub fn new(config: GitHubIntegration) -> Result<Self> {
722 let http_client = HttpClient::new("https://api.github.com".to_string())?;
723 let rate_limiter = RateLimiter::new(5000); Ok(Self {
726 config,
727 http_client,
728 rate_limiter,
729 })
730 }
731
732 pub fn send_notification(&mut self, notification: &IntegrationNotification) -> Result<()> {
734 match ¬ification.notification_type {
735 NotificationType::TestCompletion | NotificationType::TestFailure => {
736 if self.config.create_status_checks {
737 self.create_status_check(notification)?;
738 }
739 if self.config.create_pr_comments {
740 self.create_pr_comment(notification)?;
741 }
742 }
743 NotificationType::PerformanceRegression => {
744 if self.config.create_regression_issues {
745 self.create_issue(notification)?;
746 }
747 }
748 _ => {}
749 }
750
751 Ok(())
752 }
753
754 fn create_status_check(&mut self, notification: &IntegrationNotification) -> Result<()> {
756 let state = match ¬ification.notification_type {
757 NotificationType::TestCompletion => "success",
758 NotificationType::TestFailure => "failure",
759 _ => "pending",
760 };
761
762 let payload = serde_json::json!({
763 "state": state,
764 "description": ¬ification.message,
765 "context": self.config.status_checks.context
766 });
767
768 println!("GitHub Status Check: {}", payload);
770 Ok(())
771 }
772
773 fn create_pr_comment(&mut self, notification: &IntegrationNotification) -> Result<()> {
775 let comment_body = format!("## {}\n\n{}", notification.title, notification.message);
776
777 let payload = serde_json::json!({
778 "body": comment_body
779 });
780
781 println!("GitHub PR Comment: {}", payload);
783 Ok(())
784 }
785
786 fn create_issue(&mut self, notification: &IntegrationNotification) -> Result<()> {
788 let payload = serde_json::json!({
789 "title": ¬ification.title,
790 "body": ¬ification.message,
791 "labels": [self.config.labels.performance_regression]
792 });
793
794 println!("GitHub Issue: {}", payload);
796 Ok(())
797 }
798
799 pub fn get_health_status(&self) -> IntegrationStatus {
801 IntegrationStatus::Healthy
803 }
804}
805
806impl SlackClient {
807 pub fn new(config: SlackIntegration) -> Result<Self> {
809 let http_client = HttpClient::new("https://hooks.slack.com".to_string())?;
810
811 Ok(Self {
812 config,
813 http_client,
814 })
815 }
816
817 pub fn send_notification(&mut self, notification: &IntegrationNotification) -> Result<()> {
819 let color = match notification.priority {
820 NotificationPriority::Critical => "#FF0000",
821 NotificationPriority::High => "#FF8C00",
822 NotificationPriority::Normal => "#36A64F",
823 NotificationPriority::Low => "#808080",
824 };
825
826 let payload = serde_json::json!({
827 "channel": self.config.default_channel,
828 "username": self.config.username.as_deref().unwrap_or("CI/CD Bot"),
829 "icon_emoji": self.config.icon_emoji.as_deref().unwrap_or(":robot_face:"),
830 "attachments": [{
831 "color": color,
832 "title": ¬ification.title,
833 "text": ¬ification.message,
834 "timestamp": notification.timestamp.duration_since(SystemTime::UNIX_EPOCH)
835 .unwrap_or_default().as_secs()
836 }]
837 });
838
839 println!("Slack Message: {}", payload);
841 Ok(())
842 }
843
844 pub fn get_health_status(&self) -> IntegrationStatus {
846 IntegrationStatus::Healthy
848 }
849}
850
851impl EmailClient {
852 pub fn new(config: EmailIntegration) -> Result<Self> {
854 let smtp_client = SmtpClient::new(config.smtp.clone())?;
855
856 Ok(Self {
857 config,
858 smtp_client,
859 })
860 }
861
862 pub fn send_notification(&mut self, notification: &IntegrationNotification) -> Result<()> {
864 let subject = match notification.priority {
865 NotificationPriority::Critical => format!("[CRITICAL] {}", notification.title),
866 NotificationPriority::High => format!("[HIGH] {}", notification.title),
867 _ => notification.title.clone(),
868 };
869
870 let body = if self.config.templates.use_html {
871 self.create_html_email(¬ification.message)
872 } else {
873 notification.message.clone()
874 };
875
876 println!("Email - Subject: {}, Body: {}", subject, body);
878 Ok(())
879 }
880
881 fn create_html_email(&self, message: &str) -> String {
883 format!(
884 r#"<!DOCTYPE html>
885<html>
886<head><title>CI/CD Notification</title></head>
887<body>
888<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
889<h2>CI/CD Automation Notification</h2>
890<p>{}</p>
891<hr>
892<p><small>This is an automated message from the CI/CD system.</small></p>
893</div>
894</body>
895</html>"#,
896 message.replace('\n', "<br>")
897 )
898 }
899
900 pub fn get_health_status(&self) -> IntegrationStatus {
902 IntegrationStatus::Healthy
904 }
905}
906
907impl WebhookClient {
908 pub fn new(config: WebhookIntegration) -> Result<Self> {
910 let http_client = HttpClient::new(config.url.clone())?;
911 let retry_manager = RetryManager::new(config.payload.clone().into());
912
913 Ok(Self {
914 config,
915 http_client,
916 retry_manager,
917 })
918 }
919
920 pub fn should_trigger(&self, notification_type: &NotificationType) -> bool {
922 match notification_type {
923 NotificationType::TestCompletion => self.config.triggers.on_completion,
924 NotificationType::TestFailure => self.config.triggers.on_failure,
925 NotificationType::PerformanceRegression => self.config.triggers.on_regression,
926 NotificationType::PerformanceImprovement => self.config.triggers.on_improvement,
927 _ => false,
928 }
929 }
930
931 pub fn send_notification(&mut self, notification: &IntegrationNotification) -> Result<()> {
933 let payload = self.create_webhook_payload(notification)?;
934
935 println!("Webhook {} - Payload: {}", self.config.name, payload);
937 Ok(())
938 }
939
940 fn create_webhook_payload(&self, notification: &IntegrationNotification) -> Result<String> {
942 match self.config.payload.format {
943 PayloadFormat::JSON => {
944 let payload = serde_json::json!({
945 "type": format!("{:?}", notification.notification_type),
946 "title": notification.title,
947 "message": notification.message,
948 "priority": format!("{:?}", notification.priority),
949 "timestamp": notification.timestamp.duration_since(SystemTime::UNIX_EPOCH)
950 .unwrap_or_default().as_secs(),
951 "data": notification.data
952 });
953 Ok(serde_json::to_string(&payload)?)
954 }
955 PayloadFormat::XML => Ok(format!(
956 r#"<?xml version="1.0"?>
957<notification>
958 <type>{:?}</type>
959 <title>{}</title>
960 <message>{}</message>
961 <priority>{:?}</priority>
962 <timestamp>{}</timestamp>
963</notification>"#,
964 notification.notification_type,
965 notification.title,
966 notification.message,
967 notification.priority,
968 notification
969 .timestamp
970 .duration_since(SystemTime::UNIX_EPOCH)
971 .unwrap_or_default()
972 .as_secs()
973 )),
974 _ => Ok(notification.message.clone()),
975 }
976 }
977
978 pub fn get_health_status(&self) -> IntegrationStatus {
980 IntegrationStatus::Healthy
982 }
983}
984
985impl HttpClient {
986 pub fn new(base_url: String) -> Result<Self> {
988 Ok(Self {
989 base_url,
990 default_headers: HashMap::new(),
991 timeout: Duration::from_secs(30),
992 user_agent: "CI/CD-Automation/1.0".to_string(),
993 })
994 }
995
996 pub fn request(&self, method: &HttpMethod, path: &str, body: Option<String>) -> Result<String> {
998 println!("HTTP Request: {:?} {}{}", method, self.base_url, path);
1000 if let Some(body) = body {
1001 println!("Body: {}", body);
1002 }
1003 Ok("success".to_string())
1004 }
1005}
1006
1007impl SmtpClient {
1008 pub fn new(config: SmtpConfig) -> Result<Self> {
1010 Ok(Self {
1011 config,
1012 connection_pool: SmtpConnectionPool {
1013 max_connections: 5,
1014 active_connections: 0,
1015 connection_timeout: Duration::from_secs(30),
1016 },
1017 })
1018 }
1019
1020 pub fn send_email(&self, to: &[String], subject: &str, body: &str) -> Result<()> {
1022 println!("SMTP Email - To: {:?}, Subject: {}", to, subject);
1024 Ok(())
1025 }
1026}
1027
1028impl RateLimiter {
1029 pub fn new(requests_per_minute: u32) -> Self {
1031 Self {
1032 requests_per_minute,
1033 current_requests: 0,
1034 reset_time: SystemTime::now() + Duration::from_secs(60),
1035 request_history: Vec::new(),
1036 }
1037 }
1038
1039 pub fn is_allowed(&mut self) -> bool {
1041 let now = SystemTime::now();
1042
1043 if now >= self.reset_time {
1045 self.current_requests = 0;
1046 self.reset_time = now + Duration::from_secs(60);
1047 self.request_history.clear();
1048 }
1049
1050 if self.current_requests < self.requests_per_minute {
1052 self.current_requests += 1;
1053 self.request_history.push(now);
1054 true
1055 } else {
1056 false
1057 }
1058 }
1059}
1060
1061impl RetryManager {
1062 pub fn new(config: WebhookRetryConfig) -> Self {
1064 Self {
1065 config,
1066 failed_requests: Vec::new(),
1067 statistics: RetryStatistics::default(),
1068 }
1069 }
1070
1071 pub fn add_failed_request(&mut self, request_data: RequestData, error: String) {
1073 let failed_request = FailedRequest {
1074 id: uuid::Uuid::new_v4().to_string(),
1075 request_data,
1076 failed_at: SystemTime::now(),
1077 retry_attempts: 0,
1078 last_error: error,
1079 next_retry_at: SystemTime::now() + Duration::from_secs(self.config.initial_delay_sec),
1080 };
1081
1082 self.failed_requests.push(failed_request);
1083 }
1084
1085 pub fn process_retries(&mut self) -> Result<()> {
1087 let now = SystemTime::now();
1088 let mut completed_retries = Vec::new();
1089
1090 for (index, failed_request) in self.failed_requests.iter_mut().enumerate() {
1091 if now >= failed_request.next_retry_at
1092 && failed_request.retry_attempts < self.config.max_retries
1093 {
1094 println!("Retrying request: {}", failed_request.id);
1097
1098 failed_request.retry_attempts += 1;
1099 self.statistics.total_retries += 1;
1100
1101 let delay = self.config.initial_delay_sec as f64
1103 * self
1104 .config
1105 .backoff_multiplier
1106 .powi(failed_request.retry_attempts as i32);
1107 let delay = delay.min(self.config.max_delay_sec as f64) as u64;
1108
1109 failed_request.next_retry_at = now + Duration::from_secs(delay);
1110
1111 if failed_request.retry_attempts >= 2 {
1113 self.statistics.successful_retries += 1;
1114 completed_retries.push(index);
1115 }
1116 } else if failed_request.retry_attempts >= self.config.max_retries {
1117 self.statistics.failed_retries += 1;
1118 completed_retries.push(index);
1119 }
1120 }
1121
1122 for &index in completed_retries.iter().rev() {
1124 self.failed_requests.remove(index);
1125 }
1126
1127 Ok(())
1128 }
1129}
1130
1131impl Default for WebhookStatistics {
1134 fn default() -> Self {
1135 Self {
1136 requests_sent: 0,
1137 requests_successful: 0,
1138 requests_failed: 0,
1139 average_response_time_ms: 0.0,
1140 retry_stats: RetryStatistics::default(),
1141 }
1142 }
1143}
1144
1145impl Default for RetryStatistics {
1146 fn default() -> Self {
1147 Self {
1148 total_retries: 0,
1149 successful_retries: 0,
1150 failed_retries: 0,
1151 average_retry_delay_sec: 0.0,
1152 }
1153 }
1154}
1155
1156impl From<WebhookPayloadConfig> for WebhookRetryConfig {
1157 fn from(_payload_config: WebhookPayloadConfig) -> Self {
1158 Self {
1159 max_retries: 3,
1160 initial_delay_sec: 5,
1161 backoff_multiplier: 2.0,
1162 max_delay_sec: 300,
1163 }
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170
1171 #[test]
1172 fn test_notification_creation() {
1173 let notification = IntegrationNotification {
1174 notification_type: NotificationType::TestCompletion,
1175 title: "Tests Completed".to_string(),
1176 message: "All tests passed successfully".to_string(),
1177 priority: NotificationPriority::Normal,
1178 data: HashMap::new(),
1179 timestamp: SystemTime::now(),
1180 };
1181
1182 assert_eq!(
1183 notification.notification_type,
1184 NotificationType::TestCompletion
1185 );
1186 assert_eq!(notification.priority, NotificationPriority::Normal);
1187 }
1188
1189 #[test]
1190 fn test_rate_limiter() {
1191 let mut limiter = RateLimiter::new(5);
1192
1193 for _ in 0..5 {
1195 assert!(limiter.is_allowed());
1196 }
1197
1198 assert!(!limiter.is_allowed());
1200 }
1201
1202 #[test]
1203 fn test_integration_status() {
1204 assert!(IntegrationStatus::Healthy < IntegrationStatus::Warning);
1205 assert!(IntegrationStatus::Warning < IntegrationStatus::Error);
1206 }
1207
1208 #[test]
1209 fn test_notification_priority() {
1210 assert!(NotificationPriority::Low < NotificationPriority::Normal);
1211 assert!(NotificationPriority::Normal < NotificationPriority::High);
1212 assert!(NotificationPriority::High < NotificationPriority::Critical);
1213 }
1214
1215 #[test]
1216 fn test_webhook_payload_format() {
1217 let notification = IntegrationNotification {
1218 notification_type: NotificationType::TestCompletion,
1219 title: "Test".to_string(),
1220 message: "Test message".to_string(),
1221 priority: NotificationPriority::Normal,
1222 data: HashMap::new(),
1223 timestamp: SystemTime::now(),
1224 };
1225
1226 let webhook_config = WebhookIntegration {
1228 name: "test".to_string(),
1229 url: "https://example.com/webhook".to_string(),
1230 method: HttpMethod::POST,
1231 headers: HashMap::new(),
1232 auth: None,
1233 triggers: WebhookTriggerConfig {
1234 on_completion: true,
1235 on_regression: false,
1236 on_failure: false,
1237 on_improvement: false,
1238 custom_conditions: Vec::new(),
1239 },
1240 payload: WebhookPayloadConfig {
1241 format: PayloadFormat::JSON,
1242 include_results: true,
1243 include_metrics: true,
1244 include_environment: false,
1245 custom_template: None,
1246 },
1247 };
1248
1249 let client = WebhookClient::new(webhook_config);
1250 assert!(client.is_ok());
1251 }
1252}