From ab35992b87bc8d08ba328130a16212878cb459b7 Mon Sep 17 00:00:00 2001 From: DaLaw2 Date: Sun, 19 Apr 2026 14:24:16 +0800 Subject: [PATCH] refactor(notification): invert telegram dependency via factory port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NotificationService used to import `crate::adapter::telegram::TelegramAdapter` directly to construct the adapter inside `test_telegram` — a hexagonal- architecture violation (core depending on a concrete adapter type). Introduce a new `AlertNotifierFactory` port. The adapter layer provides `TelegramAdapterFactory` which implements it and instantiates a fresh `TelegramAdapter` on each `create()` call (so the test path observes the most-recently-saved DB config — the original ad-hoc construction intent is preserved). NotificationService now depends only on the port. Co-Authored-By: Claude Opus 4.7 (1M context) --- net-guardia/src/adapter/telegram/mod.rs | 25 ++++++++++++++++- net-guardia/src/core/notification_service.rs | 27 ++++++++++++++----- .../src/infrastructure/service_factory.rs | 10 +++++-- .../src/interface/port/notification.rs | 17 +++++++++++- 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/net-guardia/src/adapter/telegram/mod.rs b/net-guardia/src/adapter/telegram/mod.rs index 157d219..ab56748 100644 --- a/net-guardia/src/adapter/telegram/mod.rs +++ b/net-guardia/src/adapter/telegram/mod.rs @@ -8,7 +8,7 @@ use reqwest::Client; use tokio::time::sleep; use crate::interface::port::app_repo::AppRepo; -use crate::interface::port::notification::{AlertNotifier, AlertPayload}; +use crate::interface::port::notification::{AlertNotifier, AlertNotifierFactory, AlertPayload}; use crate::interface::port::secret_store::SecretStorePort; use crate::interface::port::setting::SettingRepo; use crate::model::config::constants::TELEGRAM_MAX_RETRIES; @@ -262,3 +262,26 @@ impl AlertNotifier for TelegramAdapter { .await } } + +/// Adapter-side factory that satisfies the `AlertNotifierFactory` port. Holds +/// the same shared dependencies the long-lived adapter uses; each `create()` +/// call instantiates a fresh `TelegramAdapter` so the test path observes +/// whatever config the user just saved. +pub struct TelegramAdapterFactory { + notif: Arc, + repo: Arc, + secrets: Option>, +} + +impl TelegramAdapterFactory { + pub fn new(notif: Arc, repo: Arc, secrets: Option>) -> Self { + Self { notif, repo, secrets } + } +} + +impl AlertNotifierFactory for TelegramAdapterFactory { + fn create(&self) -> Result, Error> { + let adapter = TelegramAdapter::new(self.notif.clone(), self.repo.clone(), self.secrets.clone())?; + Ok(Arc::new(adapter)) + } +} diff --git a/net-guardia/src/core/notification_service.rs b/net-guardia/src/core/notification_service.rs index 791acea..c868832 100644 --- a/net-guardia/src/core/notification_service.rs +++ b/net-guardia/src/core/notification_service.rs @@ -2,10 +2,9 @@ use std::sync::Arc; use serde_json::Value; -use crate::adapter::telegram::TelegramAdapter; use crate::core::email::scheduler::SmtpClient; use crate::interface::port::app_repo::AppRepo; -use crate::interface::port::notification::AlertNotifier; +use crate::interface::port::notification::AlertNotifierFactory; use crate::interface::port::secret_store::SecretStorePort; use crate::interface::port::setting::SettingRepo; use crate::model::error::Error; @@ -17,11 +16,22 @@ pub struct NotificationService { notif: Arc, repo: Arc, secrets: Arc, + alert_notifier_factory: Arc, } impl NotificationService { - pub fn new(notif: Arc, repo: Arc, secrets: Arc) -> Self { - Self { notif, repo, secrets } + pub fn new( + notif: Arc, + repo: Arc, + secrets: Arc, + alert_notifier_factory: Arc, + ) -> Self { + Self { + notif, + repo, + secrets, + alert_notifier_factory, + } } /// Get Telegram config with redacted bot_token. @@ -65,10 +75,13 @@ impl NotificationService { self.notif.set_notification_config("telegram", &config_json) } - /// Send a test Telegram message using current config. + /// Send a test Telegram message using current config. The factory + /// constructs a fresh notifier on every call so the test reflects the + /// most-recently-saved config (the user typically clicks "test" + /// immediately after `set_telegram_config`). pub async fn test_telegram(&self) -> Result<(), Error> { - let adapter = TelegramAdapter::new(self.notif.clone(), self.repo.clone(), Some(self.secrets.clone()))?; - adapter.send_test_message().await + let notifier = self.alert_notifier_factory.create()?; + notifier.send_test_message().await } /// Send a test email using current SMTP config. diff --git a/net-guardia/src/infrastructure/service_factory.rs b/net-guardia/src/infrastructure/service_factory.rs index f2083c4..990a512 100644 --- a/net-guardia/src/infrastructure/service_factory.rs +++ b/net-guardia/src/infrastructure/service_factory.rs @@ -18,7 +18,7 @@ use crate::core::auth::jwt::JwtService; use crate::adapter::access_control_adapter::EbpfAccessControlAdapter; use crate::adapter::ebpf::EbpfServices; use crate::adapter::persistence::Database; -use crate::adapter::telegram::TelegramAdapter; +use crate::adapter::telegram::{TelegramAdapter, TelegramAdapterFactory}; use crate::core::acl_service::AclService; use crate::core::config_service::ConfigService; use crate::core::dns_filter_service::DnsFilterService; @@ -45,7 +45,7 @@ use crate::interface::port::access_control_admin::AccessControlAdminPort; use crate::interface::port::app_repo::AppRepo; use crate::interface::port::dns_filter_api::DnsFilterPort; use crate::interface::port::geo_block_api::GeoBlockPort; -use crate::interface::port::notification::AlertNotifier; +use crate::interface::port::notification::{AlertNotifier, AlertNotifierFactory}; use crate::interface::port::rate_limit_api::RateLimitPort; use crate::interface::port::secret_store::SecretStorePort; use crate::interface::port::setting::SettingRepo; @@ -300,10 +300,16 @@ impl ServiceFactory { )); let config_service = Arc::new(ConfigService::new(db.clone() as Arc).with_secret_store(secret_store_port.clone())); + let notifier_factory: Arc = Arc::new(TelegramAdapterFactory::new( + db.clone() as Arc, + db.clone() as Arc, + Some(secret_store_port.clone()), + )); let notification_service = Arc::new(NotificationService::new( db.clone() as Arc, db.clone() as Arc, secret_store_port, + notifier_factory, )); let suricata_manager = SuricataManager::new(app_config.clone()); diff --git a/net-guardia/src/interface/port/notification.rs b/net-guardia/src/interface/port/notification.rs index 47c58bc..f067f59 100644 --- a/net-guardia/src/interface/port/notification.rs +++ b/net-guardia/src/interface/port/notification.rs @@ -1,6 +1,9 @@ -use crate::model::error::Error; +use std::sync::Arc; + use async_trait::async_trait; +use crate::model::error::Error; + /// Alert notification data sent by SOAR engine. #[derive(Debug, Clone)] pub struct AlertPayload { @@ -20,3 +23,15 @@ pub trait AlertNotifier: Send + Sync { async fn send_alert(&self, payload: &AlertPayload) -> Result<(), Error>; async fn send_test_message(&self) -> Result<(), Error>; } + +/// Port for constructing an `AlertNotifier` on demand. +/// +/// `NotificationService::test_telegram` needs to test the *current* DB config, +/// which can have been set after the service was wired. A pre-constructed +/// notifier wouldn't reflect the new credentials, and `core/` cannot reach +/// into `adapter/telegram` to build one. The factory inverts this: the +/// implementation lives in the adapter layer, the core service depends only +/// on this trait, and each call gets a fresh notifier reading the latest config. +pub trait AlertNotifierFactory: Send + Sync { + fn create(&self) -> Result, Error>; +}